Compare commits

...

213 Commits

Author SHA1 Message Date
Smol Ame
fc828972d8
Read Tokyo Ghoul: Update baseUrl (#9756)
All checks were successful
CI / Prepare job (push) Successful in 4s
CI / Build individual modules (push) Successful in 6m48s
CI / Publish repo (push) Successful in 43s
* Read Tokyo Ghoul: Bump versionCode

* Read Tokyo Ghoul: Update baseUrl
2025-07-25 05:47:30 +01:00
are-are-are
4630e1ba74
Update some domain (#9749)
* NetTruyenCO update domain

* LxManga update domain

* DocTruyen3Q update domain

* HentaiCB update domain

* DocTruyen5s update domain
2025-07-25 05:47:30 +01:00
KirinRaikage
9d20a590ae
Mangas-Origines: Fix chapters not found (#9741)
* Mangas-Origines: Fix chapters not found

* Add missing trailing comma
2025-07-25 05:47:29 +01:00
are-are-are
42a448d15f
Add Source MimiHentai (#9726)
* Add Mimihentai

* Delete PageDTO.kt

* Delete ChapterDTO & use getMangaUrl

* Use Page parseAs

* delete client

* used getChapterUrl as instructed stevenyomi

* use toManga

* imageUrlParse throw UnsupportedOperationException()

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Fix various issues

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
stevenyomi
cb99f8ea64
Move ColaManga to single source (#9737)
The other override, MangaDig, for which this multisrc was created, was removed in #5319.

The extension stays broken and the version code is not bumped.
2025-07-25 05:47:29 +01:00
kanoou
927507b7ac
Add LectorMOnline and MangasX (#9665)
* add lectormonline

* add webview methods

* review

* move to factory

* wut

* move to multisrc

* move iconcs to multisrc
2025-07-25 05:47:29 +01:00
kanoou
9a2e34df4d
Cartel de Manhwas: Update domain (#9733)
update domain
2025-07-25 05:47:29 +01:00
kanoou
b1e4ab83cc
MangaCrab: Update domain and fix pages not loading (#9725)
fix pages
2025-07-25 05:47:29 +01:00
kanoou
208168943c
VerComics: Add pages selector (#9724)
add selector
2025-07-25 05:47:29 +01:00
Smol Ame
143e964708
Little Tyrant: Remove www. from baseUrl (#9720)
* Little Tyrant: Bump versionCode

* Little Tyrant: Remove `www.` from baseUrl
2025-07-25 05:47:29 +01:00
are-are-are
8bd625d00b
Truyenhentai18: Fix search no image (#9717)
Fix search no image
2025-07-25 05:47:29 +01:00
tanaka-shizuku3
1f8bb317b6
Mangabz: Fix StringIndexOutOfBoundsException (#9710)
* Mangabz: Fix StringIndexOutOfBoundsException

StringIndexOutOfBoundsException happens when description is the same as title

* fix description

* remove some lazys since JVM guarantees initialization only when calling this file

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
are-are-are
b09647744c
Truyenhentai18: Fix popularMangaNextPageSelector (#9711)
Fix popularMangaNextPageSelector
2025-07-25 05:47:29 +01:00
are-are-are
36d7baeae6
NettruyenCO: Fix bug input string (#9709)
Fix input string
2025-07-25 05:47:29 +01:00
are-are-are
6fb92c5ade
LxManga: FIx dateTimeformat (#9708)
Fix dateTime & bump version
2025-07-25 05:47:29 +01:00
Hiirbaf
3018e37492
LectorTMO: Add more status (#9695)
* Add more status

* Update version
2025-07-25 05:47:29 +01:00
meatballsaretasty
42e4618a13
Fix/hc not finding pages (#9669)
* Update build.gradle

* fix: pageListParse not returning any pages

The old selector wasn't returning any images. 

Changed fetchChapterList url to the new HC `story` page which has all the images, unpaginated. 

Updated pageListParse.
2025-07-25 05:47:29 +01:00
kanoou
187fa7de52
IkigaiMangas: Fix nsfw cookie value (#9667)
fix nsfw cookie
2025-07-25 05:47:29 +01:00
Chopper
0a7189534e
SpectralScan: Fix pages (#9658)
* Fix pages

* Close the response.

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
kanoou
09bab2593b
TempleScan(es): Auto update domain (#9651)
* autoupdate domain

* use location header

* remove ci check
2025-07-25 05:47:29 +01:00
stevenyomi
46b898fcd4
Manhuagui: cleanup (#9663)
* cleanup

* bump user agent

* remove chapter number regex, let the app parse it

* bump version code

* remove extra new line
2025-07-25 05:47:29 +01:00
Alan Tan
2638c08b11
baozimhorg: fix illegal State Exception & more (#9656)
* baozimhorg: fix illegal State Exception

fix #9543

* Goda: Version bump

* Goda: Minor change

* Goda: improvement to mangaDetailsParse element selector

* Goda: added more status

* Revert "Goda: improvement to mangaDetailsParse element selector"

This reverts commit 6fe7c8b165ebf9b9568644aa50ef7d0e23a0a888.

* Goda: fix typo

* Goda: improvement to mangaDetailsParse element selector

* The selector is equally breakable

* Add mirrors

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
AwkwardPeak7
363498aee3
SchaleNetwork: simplify clearance token logic (#9630) 2025-07-25 05:47:29 +01:00
stevenyomi
87cd9dc9fb
Update MCCMS sources (#9631)
* Add back Manhuawu, closes #1567

* Clean up 6Manhua

* Add Miaoqu Manhua, closes #4482

* Add 2 French sources
2025-07-25 05:47:29 +01:00
KirinRaikage
42d0c589d6
Poseidon Scans: Update domain (#9628)
* Poseidon Scans: Update domain

* Poseidon Scans: Add missing trailing comma

* Update src/fr/poseidonscans/build.gradle

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
stevenyomi
b998f4cc58
Remove some dead sources (#9626)
* Remove Mangajikan, closes #9426

* Remove Gufeng Manhua, closes #9437

* Remove Damao Manhua (image pages all broken)
2025-07-25 05:47:29 +01:00
stevenyomi
3993e7349b
Add MMLook multisrc (#9624)
* Add MMLook multisrc

* show updated time

* fix updated text

* tweak manga url logic

* Use cloudflareClient
2025-07-25 05:47:29 +01:00
Chopper
03b8b9b4ca
TruyenHentai18: Update domain and fix loading content (#9586)
* Update domains

* Add private statement in DTO

* Add setUrlWithoutDomain in mangaDetailsParse

* Save slug without lang prefix

* Apply changes
2025-07-25 05:47:29 +01:00
Hualiang
b747b55681
Bilimanga: fixed some chapter page response encoding parsing errors (#9609)
* fixed some chapter page response encoding parsing errors

* modify
2025-07-25 05:47:29 +01:00
AwkwardPeak7
aedd777371
Dynasty: original covers & chapter metadata (#9604)
* original covers & chapter metadata

* use cached cover if new one is same
2025-07-25 05:47:29 +01:00
Romain
ea34656edf
Switch to the new URL of Rimu Scans (#9603)
* Switch url
Fix #9602

* Bump version
2025-07-25 05:47:29 +01:00
AwkwardPeak7
296a7bf55d
Hitomi: fix animated webp & potential oom (#9600)
* animated images can be webp

closes https://github.com/keiyoushi/extensions-source/issues/9554

* avoid internal resize overhead of HashSet

* bump

* import
2025-07-25 05:47:29 +01:00
AwkwardPeak7
d773d2692b
Webtoons.com: add rate limit & handle chapter count reset on new season (#9593)
* Webtoons: ratelimit chapters fetch api

closes https://github.com/keiyoushi/extensions-source/issues/9570

* handle season number reset

closes https://github.com/keiyoushi/extensions-source/issues/9495

* edge case bug

* bump
2025-07-25 05:47:29 +01:00
AwkwardPeak7
827e91d2c6
MangaDistrict: fix search (#9592) 2025-07-25 05:47:29 +01:00
Hualiang
a5a62a2d4e
Add bilimanga source (#9552)
* init

* optimize

* adjust

* little modify

* modify extName

* modify prompt

* apply commit

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* apply commit

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* add request rate limit

* apply commit

* apply commit

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
stevenyomi
2142cb32c2
Close response properly in core utility parseAs() (#9591) 2025-07-25 05:47:29 +01:00
Chopper
603fd58fff
TresDaosScan: Update domain and add url editor (#9589)
Update domain and add url editor
2025-07-25 05:47:29 +01:00
Chopper
5bdb239025
Add MangaKusu (#9588) 2025-07-25 05:47:29 +01:00
Chopper
642156da15
Remove ReadMangas (#9587) 2025-07-25 05:47:29 +01:00
stevenyomi
66551c4eca
Remove duplicate YYmanhua (#9564)
It is a mirror of Mangabz.
2025-07-25 05:47:29 +01:00
Chopper
3d797524b2
Update domains (#9559)
Rawkuma: Update domain (#9528)

* Rawkuma: Bump versionCode

* Rawkuma: Update baseUrl

Co-authored-by: Smol Ame <155411819+Smol-Ame@users.noreply.github.com>
2025-07-25 05:47:29 +01:00
Chopper
cdcde3cca1
Update domains (#9558) 2025-07-25 05:47:29 +01:00
Chopper
4050e42337
Add new multi-src: zerotheme (#9544) 2025-07-25 05:47:29 +01:00
stevenyomi
1980853506
Setup CODEOWNERS (#9580) 2025-07-25 05:47:29 +01:00
mozzaru
aa40f1101f
SirenKomik: fix 'Post ID not found' (#9579)
fix load picture fast mybe
2025-07-25 05:47:29 +01:00
Nikolaï LEMERRE
84dc72f863
Update RaijinScan URL (#9576) 2025-07-25 05:47:28 +01:00
tanaka-shizuku3
54f4552fcc
Dm5: Fix chapter list not loading (#9573) 2025-07-25 05:47:28 +01:00
are-are-are
f6945c3b71
Update some domain (#9562)
* TruyenGG update domain

* Sayhentai update domain

* NettruyenCO update domain

* GocTruyenTranh update domain

* TopTruyen update domain
2025-07-25 05:47:28 +01:00
Chopper
dd8c469abf
Add MangaStop (#9553) 2025-07-25 05:47:28 +01:00
stevenyomi
3297983a49
Jinman Tiantang: update domains (#9548)
* Update JinmantiantangPreferences.kt

* Update AndroidManifest.xml

* Update build.gradle

* Update JinmantiantangPreferences.kt

* Update URL
2025-07-25 05:47:28 +01:00
Hualiang
3b64f94ff8
Komiic: Add search filter (#9531)
* add comic description

* fix manga search results missing descriptions

* clean unused variables

* clean unused class

* Add some config options and refactor some code

* refactor some code

* modify config option summary

* apply comments

* modify Queries.kt

* small modification

* Format code

* Format code

* replace parse method

* optimize check API limit

* modify config summary

* add search filter

* add getChapterUrl()

* refactor Query.kt

* use filters.firstInstance()

* nothing

* Replace require() with check()
2025-07-25 05:47:28 +01:00
Smol Ame
5ec031e3b8
Rawkuma: Update domain (#9528)
* Rawkuma: Bump versionCode

* Rawkuma: Update baseUrl
2025-07-25 05:47:28 +01:00
peakedshout
1cbbb8911e
Jinman Tiantang: manga details resolve (#9522)
* Fix: manga details resolve

* Fix: manga details resolve
2025-07-25 05:47:28 +01:00
Chopper
a5a70f4a31
Add new madara sources (#9489)
* Add madara source

* Remove icon 512.png
2025-07-25 05:47:28 +01:00
Chopper
0284d48333
YugenMangas: Update domain (#9477)
* Update domain

* Fix
2025-07-25 05:47:28 +01:00
ilona
b860b15286
Feat/pixiv deeplink (#9457)
* Pixiv: added deeplink and ID search (#9452)

Direct ID search is triggered by prefixing the item's ID with with `aid:` for artworks/illustrations, `sid:` for series, and `user:` for users. The former two are meant only for use in deeplinks, while the latter may also be useful for actual users and therefore has a more exposable name. (All of these prefixes are subject to change)

* Pixiv: bandaid fix for API not returning needed values (#9452)

Apparently depending on the circumstances, the API doesn't return the user ID to which a series belongs. This user ID is instead placed in the outer Illustration object. This very basic (and subject to a larger refactoring) fix ensures that the user ID is always present when needed to construct the link (it isn't required for anything else in the API)

* Pixiv: ensured that only exact matches to the deeplink patterns are handled specially (#9452)

The exact pattern is: `<type>:<ID consisting of digits>`. By ensuring that only digits and nothing else afterwards are allowed by the pattern matching (otherwise falling back to regular search), we further decrease the likelihood of users accidentally triggering this functionality (it sadly can't be entirely avoided, since deeplinks need to share an interface with the regular search queries)

* Pixiv: changed Deeplink system to use URL

Instead of complex parsing logic in the (deliberately lightweight and kotlin-wise handicapped) Deeplink Activity, the captured URL can just be passed to the search directly and handled there. The ability for the search to understand full Pixiv URLs is useful (and half-expected) either way, and it will not interfere with regular search function.

* Pixiv: fixed IndexOOB when query is empty/not a valid URI

* Pixiv: Applied suggestion to use OkHttp Urls
2025-07-25 05:47:28 +01:00
Hualiang
2d0e57517e
Komiic: Add manga description and refactor some code (#9445)
* add comic description

* fix manga search results missing descriptions

* clean unused variables

* clean unused class

* Add some config options and refactor some code

* refactor some code

* modify config option summary

* apply comments

* modify Queries.kt

* small modification

* Format code

* replace parse method

* optimize check API limit

* modify config summary

* add getChapterUrl()
2025-07-25 05:47:28 +01:00
Chopper
3070ed4967
HiveScans: Fix page list (#9488)
* Fix page list

* Bump version
2025-07-25 05:47:28 +01:00
Chopper
50e1b1c9fc
ImperioDaBritannia: Fix null pointer (#9487)
Fix null pointer
2025-07-25 05:47:28 +01:00
Chopper
2512c9e0ad
SpectralScan: Fix pages not found (#9479)
Fix pages not found
2025-07-25 05:47:28 +01:00
Chopper
a916378f4e
Update some domains (#9478) 2025-07-25 05:47:28 +01:00
Hualiang
5260aff425
Add mh1234 source (#9444)
* add mh1234 source

* add manga description

* apply comments
2025-07-25 05:47:28 +01:00
Chopper
746243a820
PinkRosa: Fix loading content (#9463)
Fix loading content
2025-07-25 05:47:28 +01:00
Chopper
7cc89b1d4a
SpectralScan: Fix chapters not found (#9462)
* Fix chapters not found

* Fix lint
2025-07-25 05:47:28 +01:00
Granite Bagas
4c9e52353a
fix: update some domain (#9432) 2025-07-25 05:47:28 +01:00
Nam Anh
b7b69b73fa
MayoTune: Change to multi language (#9431) 2025-07-25 05:47:28 +01:00
Chopper
e9a8b1b19a
Astratoons: Fix no pages found (#9451)
Fix no pages found
2025-07-25 05:47:28 +01:00
Chopper
791d203f42
SpectralScan: Fix no pages found (#9450)
* Fix no pages found

* Bump version
2025-07-25 05:47:28 +01:00
AlphaBoom
717d58a63c
Bakamh: Add missing http header (#9425) 2025-07-25 05:47:28 +01:00
AlphaBoom
e28e6ed77f
MangaGun: Fix Chapter list parsing (#9424) 2025-07-25 05:47:28 +01:00
scb261
63ff937ae4
Holonometria: update URL and other fixes (#9400)
* Holonometria: update URL and other fixes

* Review suggestions
2025-07-25 05:47:28 +01:00
AwkwardPeak7
e84d87a883
Hitomi: retry on stream reset exception with internal error from server (#9414)
* retry with delay on internal_error from server

* bump

* else
2025-07-25 05:47:28 +01:00
Cuong-Tran
d482b55bf5
Komik-cast: update domain (#9411) 2025-07-25 05:47:28 +01:00
Hualiang
773613ee71
Add Zazhimi source (#9364)
* add zazhimi source

* add header

* null

* apply comments

* apply comments
2025-07-25 05:47:28 +01:00
spicemace
4e0a48fff7
Kemono fix image url path (#9396) 2025-07-25 05:47:28 +01:00
Chopper
53ff72f22c
AnimeXNovel: Fix loading chapters (#9392)
Fix loading chapters
2025-07-25 05:47:28 +01:00
Chopper
ee6ec2bd75
CosmicScansID: Add url editor (#9374)
Add url editor for cosmicscans
2025-07-25 05:47:28 +01:00
Chopper
cc63389835
GreenShit: Fix (#9357)
* Add token generator and add login

* Use T.toJsonString

* Use buildJsonObject

* Use buildJsonObject in login
2025-07-25 05:47:28 +01:00
Chopper
074a0d7563
Update domain (#9371)
* Update domain

* Add more updates

* Add more updates
2025-07-25 05:47:28 +01:00
Chopper
86f9aa6c7e
SpectralScan: Update domain (#9368)
Update domain
2025-07-25 05:47:28 +01:00
Chopper
a07358aaa1
TiaManhwa: Update mangaSubString (#9354)
* Update mangaSubString

* Update mangaSubString
2025-07-25 05:47:28 +01:00
Chopper
50eac7a152
SpectralScan: migrate to new site (#9360)
Migration
2025-07-25 05:47:28 +01:00
Chopper
9995c4be38
Remove dead source (#9358) 2025-07-25 05:47:28 +01:00
Chopper
04c7052e01
Add Astrotoons (#9355)
* Add Astrotoons

* Add manga status
2025-07-25 05:47:28 +01:00
AwkwardPeak7
dd47332ab9
Webtoons.com: fixes (#9349)
* better compatibility with old urls

* use index for chapter number as fallback

* better chapter number logic

* escape chapter names

* fix deep link for canvas

* Update build.gradle

* bgm

* deeplink

* i + 1
2025-07-25 05:47:28 +01:00
AwkwardPeak7
621dc6c121
Remove kdt & add Armageddon (#9346)
remove kdt & add Armageddon
2025-07-25 05:47:28 +01:00
Troy Liu
6ee4ab2521
lanraragi: FIX: 404 error when baseurl contains additional path (#9340) 2025-07-25 05:47:28 +01:00
AwkwardPeak7
2fd8684f2e
west manga (id): update for new site (#9336)
* west manga: update for new site

* signature

* review changes

* some fields can be nullable

* encode urls with `HttpUrl.Builder`
2025-07-25 05:47:27 +01:00
Maxim Molochkov
30e8ccc669
Comx: add ability to change domains (#9328)
* Comx: add ability to change domains

* Add auto update of current domain if default domain has changed
2025-07-25 05:47:27 +01:00
Hualiang
9fb63b4f72
YYmanhua: Add the latest chapter upload date (#9334)
Add the latest chapter upload date
2025-07-25 05:47:27 +01:00
AwkwardPeak7
4d61698687
Webtoons.com: use api to get chapters (#9332)
* Webtoons.com: use api to get chapters

* completed status

* rename
2025-07-25 05:47:27 +01:00
Aurel
68df5d3b69
Fix PerfScan : Complete rewrite for new site and API (#9310)
* Refactor PerfScan extension: update base URL, remove unused theme package, and implement new API response models

* Fix Review

* Fix consistency on URL
2025-07-25 05:47:27 +01:00
FlaminSarge
8a9231c5af
Mangadex: Handle missing isUnavailable chapter field (#9301)
* Mangadex: Handle missing isUnavailable chapter field

* Update build.gradle

* Switch from nullable bool to bool for isUnavailable, with default false

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-07-25 05:47:27 +01:00
Vetle Ledaal
a9aa4705d2
BacaKomik: update domain, skip popular/latest redirect (#9270) 2025-07-25 05:47:27 +01:00
Andika Perdana D.S.
42579db7fe
update base URL for KomikIndo.co extension (#9304) 2025-07-25 05:47:27 +01:00
Andika Perdana D.S.
152bc793d2
Update URL Ikiru (#9302)
Update base URL and version code for Ikiru extension
2025-07-25 05:47:27 +01:00
superchanuwu
d89b95a0f2
CosmicScansID: Update domain (#9292)
* Update build.gradle

CosmicScansID: Update domain

* Update CosmicScansID.kt

CosmicScansID: Update domain

* Update src/id/cosmicscansid/build.gradle

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-07-25 05:47:27 +01:00
Zero
c2c4cff935
Updated Moon daisy scan domain (#9287)
* Update GhostScan.kt

* Update build.gradle

* Update MoonDaisyScans.kt

* Update build.gradle
2025-07-25 05:47:27 +01:00
peakedshout
1b46f1f396
Jinmantiantang: SChapter url select change (#9284)
Fix: SChapter url select change
2025-07-25 05:47:27 +01:00
Aurel
325772c8ab
Fix (X-manga) : Update X-manga URL (#9283)
Update Url
2025-07-25 05:47:27 +01:00
Chopper
67b795a5b8
YushukeMangas: Fix manga id not found (#9279)
Fix manga id not found
2025-07-25 05:47:27 +01:00
peakedshout
6af9f2d853
SakuraManhwa: Added support for SakuraManhwa source (#9195)
New: Added support for SakuraManhwa source
2025-07-25 05:47:27 +01:00
heddxh
8e7146ec24
Fix Comic Growl (#9009)
* fetch popular mangas

* fetch pages

* descramble image

* clean code

* fix date time parse

* get all chapters

* add dto for page response

* move dto and descrambler into separate files

* happily use parseAs

* add dummy url for missing chapters

* set different icons for lock and pay chapters

* search and latest

* get all authors

* clean code

* remove comment and unneeded json field

* fix incorrectly http url conversion
2025-07-25 05:47:27 +01:00
AwkwardPeak7
5fb99e11eb
webtoons.com (ID), preserve old source id 2025-07-25 05:47:27 +01:00
Zero
cdaf1493cd
Updated Ghostscan domain (#9256)
* Update GhostScan.kt

* Update build.gradle
2025-07-25 05:47:24 +01:00
Chopper
04a963a59a
Manhastro: Fix cookie lifetime (#9255)
* Fix cookie lifetime

* Move class to file

* Rename

* Use const
2025-07-25 05:47:24 +01:00
AwkwardPeak7
a4347e9da1
Webtoons.com: refactor and fix for site changes (#9245)
* Webtoons Translate: move out of multisrc & rework

it basically override everything from the main webtoons class, so split it off

* DongmanManhua: move to individual

* Webtoons: fix and make individual

* remove old multisrc

* use meta og:image

* deeplink fix

* fix deeplink crash & old details thumbnails
2025-07-25 05:47:24 +01:00
are-are-are
a9176c529b
NhatTruyen update domain & fix missing chapter & fix search (#9175)
* bump version

* Update domain and Fix missing chapter

* fix build

* Use suggest

* Fix Search no results

* ¯\_(ツ)_/¯

* Change fetchChapterList to chaplistRequest
2025-07-25 05:47:24 +01:00
Smol Ame
205bf49af7
Manhua Plus: Fix No Results Found & search error (#9248)
* Manhua Plus: Bump versionCode

* Manhua Plus: Set `LoadMoreStrategy` to Never
2025-07-25 05:47:24 +01:00
Eshlender
53f87f108e
[RU]Senkuro new domains (#9244) 2025-07-25 05:47:24 +01:00
kanoou
dd595cca72
EternalMangas: Move to es and change theme (#9243)
move to ES
2025-07-25 05:47:24 +01:00
FlaminSarge
66f2a0ed6e
MangaDex: Add setting to include unavailable chapters in chapter list (#9208)
* MangaDex: Add setting to include unavailable chapters in chapter list

* Remove redundant isUnavailable check
2025-07-25 05:47:24 +01:00
Chopper
6022ef39be
ArgosScan: Fix Unexpected JSON token error (#9193)
* Fix

* Change contains value

* Fixes
2025-07-25 05:47:24 +01:00
Aurel
0c3f9f2736
Fix (Raijin Scans) : Update for site changes (#9172)
* Refactor RaijinScans extension: update to HttpSource and add LatestUpdatesDto class for new site

* Fix VersionCode

* Fix review

* Fix version Code
2025-07-25 05:47:24 +01:00
DokterKaj
fcc13a63ed
DeviantArt: Preserve token in multi-image posts (#9151)
* DeviantArt: Preserve token in multi-image posts

* DeviantArt: Use keiyoushi.utils.tryParse

* DeviantArt: Make when block nicer

* DeviantArt: No need for two variables

* DeviantArt: Only find firstImageUrl if needed

* DeviantArt: Use if expression for less indenting
2025-07-25 05:47:24 +01:00
Radon Rosborough
8c6f4bfbcb
SMBC: Handle 500 "error" on archive page (#9003)
* SMBC: Handle 500 "error" on archive page

* Update based on code review

* Update based on code review
2025-07-25 05:47:24 +01:00
AwkwardPeak7
49ecc98bce
Remove usage of okhttp internal variables (#9216)
* Remove usage of okhttp internal variables

inaccessable or removed in new version of okhttp

* lint

* lint :2
2025-07-25 05:47:24 +01:00
morallkat
3244e3fe53
zh/manwa: Add status indicator (#9220)
zh/manwa: add status indicator
2025-07-25 05:47:24 +01:00
Vetle Ledaal
7b84e27eae
Update domain for Manhwa Toon (#9221) 2025-07-25 05:47:24 +01:00
AwkwardPeak7
e54cab639c
Wolf.com (ko): add referer (#9187)
* wolf.com: add referer

* use util methods
2025-07-25 05:47:24 +01:00
AlphaBoom
8a81d39865
MyComic: Support chapter groups parsing (#9186)
* MyComic: Support chapter groups parsing

* Rename dto file name for suitable camel case.
2025-07-25 05:47:24 +01:00
DokterKaj
d1878c4183
MayoTune: Use dark background for icons (#9159) 2025-07-25 05:47:24 +01:00
Nam Anh
e3b94d0c75
MayoTune: Build chapter page using API (#9157)
* Build manga page via API

* Split files

* Query chapters using `id`

* Use both query parameters
2025-07-25 05:47:24 +01:00
zhongfly
3f92fc85a0
Rumanhua: limit search keyword length to 12 (#9156)
* Rumanhua: limit search keyword length to 12

* Update build.gradle
2025-07-25 05:47:24 +01:00
Luckyanets Eugene
ee69665a7c
Fixed domain name for mangaonelove (#9141)
* Fixed domain name for mangaonelove

Signed-off-by: Eugene Luckyanets <leugenea@gmail.com>

* Bumped overrideVersionCode up to 2

Signed-off-by: Eugene Luckyanets <leugenea@gmail.com>

* Fixed domain name for mangaonelove in build.gradle

Signed-off-by: Eugene Luckyanets <leugenea@gmail.com>

---------

Signed-off-by: Eugene Luckyanets <leugenea@gmail.com>
2025-07-25 05:47:24 +01:00
peakedshout
a07cc9f52b
MomonGA: add source (#9122) 2025-07-25 05:47:24 +01:00
Jake
32a264c4bb
Readcomiconline - Update config.json (#9169)
Update config.json
2025-07-25 05:47:24 +01:00
Jake
f6820edcbd
Readcomiconline - Update config.json (#9150)
Update config.json
2025-07-25 05:47:24 +01:00
Aurel
82330730a2
[multisrc/pizzareader] Fix: Handle null status from API (#9144)
update PizzaReader to handle comic status correctly
2025-07-25 05:47:24 +01:00
Aurel
0cec461ff8
fix (Poseidonscans/PhenixScans) : Update Poseidon Scans and Phenix Scans (#9143)
* fix (PhenixScans): correct API base URL and improve chapter naming format in PoseidonScans

* update version codes
2025-07-25 05:47:24 +01:00
Chopper
026666bc38
ArgosScan: Fixes (#9135)
* Fix

* Use relative manga url
2025-07-25 05:47:24 +01:00
Chopper
d73a90d970
WeebCentral: add support for deeplink (#9129)
* Add support for deeplink

* Bump version

* Refactoring

* Add suggested changes

* Remove empty search
2025-07-25 05:47:24 +01:00
peakedshout
48f590df0c
New: Added support for rumanhua source (#9128)
* New: Added support for rumanhua source

* New: Added support for rumanhua source

* New: Added support for rumanhua source

* New: Added support for rumanhua source
2025-07-25 05:47:24 +01:00
Nam Anh
ef9d26cfe8
Add MayoTune (#9106)
* Add MayoTune

* Use fallback for text contents

* Use DTO and refactor

* Use `selectFirst` for status

* Update fallback thumbnail URL

* Lint code

* Implement as single source

* Encapsulate `ChapterDto`

* Use relative URL

* Correctly handle request endpoints
2025-07-25 05:47:24 +01:00
peakedshout
5406227f0f
Manwa: Optimize implementation logic (#9103)
* Optimization:
1. Dynamically obtain the list of optional image sources;
2. Dynamically obtain the redirected access URL;
3. Reconstruct some access logic;
4. Support filtering and next page functions;

* Optimization:
1. Dynamically obtain the list of optional image sources;
2. Dynamically obtain the redirected access URL;
3. Reconstruct some access logic;
4. Support filtering and next page functions;

* Optimization:
1. Dynamically obtain the list of optional image sources;
2. Dynamically obtain the redirected access URL;
3. Reconstruct some access logic;
4. Support filtering and next page functions;

* Optimization:
1. Dynamically obtain the list of optional image sources;
2. Dynamically obtain the redirected access URL;
3. Reconstruct some access logic;
4. Support filtering and next page functions;
2025-07-25 05:47:24 +01:00
Chopper
31e167dd72
MaidScan: fix loading content (#9133)
Fix
2025-07-25 05:47:24 +01:00
Chopper
62bfe372b2
TempestScans: update domain (#9132)
Update domain
2025-07-25 05:47:24 +01:00
Chopper
9b39822d7f
TempleScanEsp: update domain (#9131)
Update domain
2025-07-25 05:47:23 +01:00
Chopper
aa8ab20b95
TempleScan: add random UA (#9130)
Add random UA
2025-07-25 05:47:23 +01:00
Chopper
801106dee5
Add GeassComics (#9074)
* Add GeassComics

* Add suggested changes
2025-07-25 05:47:23 +01:00
Jake
748b8c4b11
Readcomiconline - Update config.json (#9127)
Update config.json
2025-07-25 05:47:23 +01:00
Jake
76fdb2499f
Readcomiconline - Update config.json (#9102)
* Update config.json

* Fix page count
2025-07-25 05:47:23 +01:00
kanoou
8b5bb2643d
Remove SenpaiEdiciones (#9098)
remove

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
2025-07-25 05:47:23 +01:00
Aurel
a5e2da61e2
Update PhenixScans extension (#9093)
Update PhenixScans extension: increment version code to 34 and filter chapters by price
2025-07-25 05:47:23 +01:00
are-are-are
5cf4e9de71
NhatTruyenS (unoriginal) update domain (#9091)
NhattruyenSS update domain
2025-07-25 05:47:23 +01:00
Chopper
51b5663380
YugenMangas: Change latestUpdate (#9080)
Change latestUpdate
2025-07-25 05:47:23 +01:00
lamaxama
73d880b915
Mangabz: Fixed NPE when searching for "as" (#9068)
* Mangabz: Fixed NPE when searching for "as"

* bump

* Update MangabzTheme.kt
2025-07-25 05:47:23 +01:00
are-are-are
9814a9770d
Vlogtruyen: Update search add filter by genre, status, sort by (#9069)
Update search, add filter genre & status, orderby
2025-07-25 05:47:23 +01:00
Chopper
db240799f0
YugenMangas: Fix loading content (#9063)
* Fix loading content

* Move regex to companion object
2025-07-25 05:47:23 +01:00
are-are-are
2a2157d48b
Manhuarock domain name update & fix chapter date upload and manga status (#9049)
* Manhuarock domain name update & minor changes

* use tryParse

* Update Missing Genre

* Fix build
2025-07-25 05:47:23 +01:00
Ardit
57cf47f154
Add Mehgazone and remove Latisbooks (#9040)
* Add initial version of mehgazone

* Update Mehgazone and remove Latisbooks

Latisbooks now redirects to Mehgazone

* Update Mehgazone.kt

* implement requested changes

* implement requested changes
2025-07-25 05:47:23 +01:00
Chopper
30f9521ed0
Add BRYaoi (#9062)
* Add BRYaoi

* Fix typo
2025-07-25 05:47:23 +01:00
Chopper
7bd7416531
CosmicScansID: Update domain (#9061)
Update domain
2025-07-25 05:47:23 +01:00
Atasshe
86727e4cde
Add search query filtering for Honkaiimpact extension (#8781)
* Add search query filtering for Honkaiimpact extension

* removed interceptors, added searchMangaParse

* Use fetchSearchManga for search query filtering to avoid race conditions

* Update src/en/honkaiimpact/src/eu/kanade/tachiyomi/extension/en/honkaiimpact/Honkaiimpact.kt

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

* Update src/en/honkaiimpact/src/eu/kanade/tachiyomi/extension/en/honkaiimpact/Honkaiimpact.kt

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

* Update src/en/honkaiimpact/src/eu/kanade/tachiyomi/extension/en/honkaiimpact/Honkaiimpact.kt

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

* Update src/en/honkaiimpact/src/eu/kanade/tachiyomi/extension/en/honkaiimpact/Honkaiimpact.kt

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

* Update src/en/honkaiimpact/src/eu/kanade/tachiyomi/extension/en/honkaiimpact/Honkaiimpact.kt

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-07-25 05:47:23 +01:00
Romain
9b9440e3e9
Add TercoScans (#8322)
* Add TercoScans

* Remove useless redirect.

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

* New day, new url

* Let's pray very hard that the url doesn't change any more

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
Co-authored-by: Prem Kumar <60751338+prem-k-r@users.noreply.github.com>
2025-07-25 05:47:23 +01:00
Hualiang
93730a008d
Add YYmanhua source (#8962)
* Add YYmanhua source

* fix some bugs and update source name

* add query filter

* Adopt the suggestions

* set user-agent and update version code

* Update src/zh/yymanhua/build.gradle

update version code

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

* Update src/zh/yymanhua/src/eu/kanade/tachiyomi/extension/zh/yymanhua/YYmanhua.kt

fix incorrect variable names

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

* Update src/zh/yymanhua/src/eu/kanade/tachiyomi/extension/zh/yymanhua/YYmanhua.kt

fix incorrect variable names

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

* Update src/zh/yymanhua/src/eu/kanade/tachiyomi/extension/zh/yymanhua/YYmanhua.kt

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

* Update src/zh/yymanhua/src/eu/kanade/tachiyomi/extension/zh/yymanhua/YYmanhua.kt

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

* Adopt the suggestions

* Adopt the suggestions

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-07-25 05:47:23 +01:00
Jake
f8d70a5252
Readcomiconline - Fix Script String Encoding (#9054)
Fix script string encoding
2025-07-25 05:47:23 +01:00
lamaxama
3b4759ef5a
VIZ: fix chapters in reverse order for logged-in users. (#9051)
* VIZ: fix chapters in reverse order for logged-in users.

* bump
2025-07-25 05:47:23 +01:00
are-are-are
22f8330387
Update some domain (#9050)
* HentaiVNPlus update domain

* TruyenVN update domain

* Vlogtruyen update domain

* HentaiCB update domain

* Sayhentai update domain

* DocTruyen3Q update domain

* TopTruyen update domain

* Teamlanhlung update domain
2025-07-25 05:47:23 +01:00
renovate[bot]
8925b685e9
Update dependency gradle to v8.14.1 (#8959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 05:47:23 +01:00
bapeey
62212e931d
Catharsis World: Update domain and logo (#9045)
update domain and logo
2025-07-25 05:47:23 +01:00
bapeey
a3532119f8
Rebrand Jobsibe to Lmto and fix domain (#9044)
fix domain and rebrand
2025-07-25 05:47:23 +01:00
bapeey
e55c893b6d
MadTheme: Retry image load with another CDN (#9047)
fallback to cdn

Co-authored-by: Vetle Ledaal <13540478+vetleledaal@users.noreply.github.com>
2025-07-25 05:47:23 +01:00
AlphaBoom
1ad39b7ab6
Add CartoonMad (#8987)
* Add CartoonMad

* Apply suggestions from code review

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

* Apply review suggestions

* Apply review suggestions

* Use encodedPath

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-07-25 05:47:23 +01:00
AlphaBoom
3a0f6ddddf
Add MyComic (#8986)
* Add MyComic

* MyComic: Replace Object filter to class filter

* Apply suggestions from code review

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

* Apply review suggestions.

* Try to use selectFirst as much as possible.

* Apply review suggestions

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-07-25 05:47:23 +01:00
AlphaBoom
af50939f4e
Happymh: Support search filters (#8985)
* Happymh: Fix chapter order

* Happymh: Support filters

* Rename header to headers for clarity

* Apply review suggestions
2025-07-25 05:47:23 +01:00
Aurel
5d970dab5a
Update PoseidonScans for the new site (#8870)
* Update PoseidonScans for the new site

* Add PoseidonScans DTOs and fix all issues

* Update PoseidonScans refactor

* Refactor PoseidonScans to add DTO classes
2025-07-25 05:47:23 +01:00
Chopper
b364d56096
Remove SlimeRead (#9028)
Remove slimeread
2025-07-25 05:47:23 +01:00
Chopper
adaca0c48e
WeebCentral: Fix thumbnail (#9021)
* Fix thumbnail

* Bump version
2025-07-25 05:47:23 +01:00
Radon Rosborough
ee40d4e9e4
Migrate from fakeimg.pl to fakeimg.ryd.tools (#9020) 2025-07-25 05:47:23 +01:00
Smol Ame
866eeef617
TonizuToon: Update domain & status selector (#9018)
* TonizuToon: Bump versionCode

* TonizuToon: Update baseURL

* TonizuToon: Update status selector
2025-07-25 05:47:23 +01:00
Smol Ame
d5301985cf
MG Komik: Update domain (#9017)
* MG Komik: Bump versionCode

* MG Komik: Update baseURL
2025-07-25 05:47:23 +01:00
Smol Ame
84efd49e17
LxManga: Update domain (#9016)
* LxManga: Bump versionCode

* LxManga: Update defaultBaseUrl

* LxManga: Update AndroidManifest
2025-07-25 05:47:23 +01:00
Chopper
b1aee99028
ImperioDaBritannia: Fix popular and latest (#9015)
Fix popular and latest
2025-07-25 05:47:23 +01:00
Liam Rooney
989bb4252b
fix: change how Iken multisrc gets latest (#9002) 2025-07-25 05:47:23 +01:00
Vetle Ledaal
5edb0c0e62
MangaPoisk: fix search encoding (#8983) 2025-07-25 05:47:23 +01:00
Vetle Ledaal
c7b13eedb1
MangaChan: fix search encoding (#8982) 2025-07-25 05:47:23 +01:00
Vetle Ledaal
597cbcce98
HenChan: fix search encoding, add headers (#8981)
* HenChan: fix search encoding

* HenChan: add headers everywhere

* HenChan: bump
2025-07-25 05:47:23 +01:00
Vetle Ledaal
2c457fbd67
YaoiChan: fix search encoding (#8980) 2025-07-25 05:47:23 +01:00
Vetle Ledaal
40f3502d10
Desu (RU): fix search encoding, add headers (#8979)
* Desu (RU): fix search encoding

* Desu (RU): add headers everywhere

* Desu (RU): bump
2025-07-25 05:47:23 +01:00
Vetle Ledaal
df9a8029d3
Manga-TR: fix search encoding (#8978) 2025-07-25 05:47:22 +01:00
Vetle Ledaal
a5560df661
Sadscans: fix search encoding (#8977) 2025-07-25 05:47:22 +01:00
Vetle Ledaal
9d94e1e704
Manga Ship: fix search encoding (#8976) 2025-07-25 05:47:22 +01:00
Vetle Ledaal
736d628518
MangaDenizi: fix search encoding (#8975) 2025-07-25 05:47:22 +01:00
Chopper
abe89d454c
Fix GreenShit (#9005)
* Fix greenshit

* Add applyIf

* Fix mediocre

* Remove applyIf
2025-07-25 05:47:22 +01:00
Chopper
ab711b37ed
Add NextScan (#8984)
* Add NextScan

* Fix status

* Bump version

* Use network.cloudflareClient

* Fix lint

* AndroidManifest.xml change pathPattern

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

* EtoshoreUrlActivity: Fix log tag

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

* URL_REGEX: Remove unneed wrapping

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-07-25 05:47:22 +01:00
laonhao139
bb92f316fc
Add Xiutaku source (#8949)
* Add Xiutaku source

* Update following comments

* Update timezone

* Update following suggestions

* Update locator

* Update following suggestions
2025-07-25 05:47:22 +01:00
theAutotelicX
d197cd7480
Add Onemanga (#8989)
* Add OneManga

* Fix cant read manga

* Fix search function
2025-07-25 05:47:22 +01:00
theAutotelicX
8076818893
Update domain moodtoon (#8990)
* Add Moodtoon

* Moodtoon: Update URL
2025-07-25 05:47:22 +01:00
Cuong-Tran
453a20d067
SpyFakku: Support Latest browsing (#8993)
support Latest
2025-07-25 05:47:22 +01:00
Smol Ame
12495730dc
JeazScans: Bump versionCode (#8995) 2025-07-25 05:47:22 +01:00
Radon Rosborough
a5c2daa5f1
[skip ci] Add *.jks to .gitignore (#9004) 2025-07-25 05:47:22 +01:00
bapeey
ad73c491fa
Add 6ianfranc9 (#8969)
add 6ianfranc9
2025-07-25 05:47:22 +01:00
bapeey
5137bb670c
Remove Taikutsu (#8968)
remove
2025-07-25 05:47:22 +01:00
bapeey
5d1f59089a
Add MangaClub (#8967)
add mangaclub
2025-07-25 05:47:22 +01:00
Yush0DAN
fc52bba37e
Taurus FanSub: Update domain (#8960)
Update domain
2025-07-25 05:47:22 +01:00
Chopper
b4fede6f9b
Update domains and migrate sources (#8958)
* JeazScans: Migrate to MangaThemesia

* TeamLanhLung: Update domain

* MagusManga: Migrate to Iken
2025-07-25 05:47:22 +01:00
Jake
dc087a1ebb
Readcomiconline - Update config.json (#8952)
Update config.json
2025-07-25 05:47:22 +01:00
Jake
9386947cc2
Readcomiconline - Update Config and Fix Preference Errors (#8944)
* unscuff code, update regexes as configurable

* Update default values, improved pref

* Update Readcomiconline.kt

Add placeholders for future implementation

* Updated page list parsing to use quickjs

* add json config, remove decryption class

* review changes, updated default config path

* review changes, lint

* lint

* lint...

* Fix RCO

* Fix post decrypt eval
2025-07-25 05:47:22 +01:00
Norsze
c464ba31b9
Update source request template description (#8939)
Swap Tachiyomi for Keiyoushi.
2025-07-25 05:47:22 +01:00
Norsze
5ee55edbef
Update issue template (#8938)
Removed link to tachiyomi repository and changed the description of the template.
2025-07-25 05:47:22 +01:00
Tejas Sharma
36295f9b69
Comick: option to de-duplicate chapters based on "score" (#8923)
* Adds the ability to automatically filter chapters in comick based on their "score"

* wording update, version bump

* use chapter_ prefix when referring to score filtering

* Update src/all/comickfun/assets/i18n/messages_en.properties

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

* change syntax for filtering based on PR feedback

* figured out how to make an extension

* updated, thanks!

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-07-25 05:47:22 +01:00
Chopper
c8cc594055
DuaLeoTruyen: Update domain and add custom settings (#8900)
* Update domain and add custom settings

* Add check to the default base url saved when the extension is updated
2025-07-25 05:47:22 +01:00
renovate[bot]
4b44545e76
Update dependency gradle to v8.14 (#8655) 2025-07-25 05:46:59 +01:00
FourTOne5
1b73235d3a
Merge :core and :utils (#8927) 2025-07-25 05:46:59 +01:00
More_Than_Hater
0532069813
ResetScans: BaseUrl update (#8921)
baseurl update

updated baseurl and versioncode for update
2025-07-25 05:46:59 +01:00
Yush0DAN
fcff393118
ManhwaWeb: Fix covers/thumbnail (#8918)
Fix covers/thumbnail
2025-07-25 05:46:59 +01:00
Chopper
02c4d7b2a8
Remove SSReading (#8913) 2025-07-25 05:46:59 +01:00
Chopper
60931a9372
WindScan: Theme change (#8912)
Migration
2025-07-25 05:46:59 +01:00
Smol Ame
06b3b60a31
Remove Anata no Motokare (#8909) 2025-07-25 05:46:59 +01:00
CriosChan
a5e30ee462
Add BlueSolo (#8902)
* Add BlueSolo support

* Changes asked by choppeh
2025-07-25 05:46:59 +01:00
Chopper
24ba7e3c0c
MonzeeKomik: Update domain and fix pages (#8899)
Update domain and fix pages
2025-07-25 05:46:59 +01:00
Chopper
8b7d0ea342
YomuMangas: Restore extension (#8877)
* Revert "Remove YomuMangas (#3803)"

This reverts commit 5e39c4e0f92c7bebc693e1d70d6ce25386c4acf0.

* Fix loading content

* Bump version

* Use tryParse
2025-07-25 05:46:59 +01:00
852 changed files with 13483 additions and 6302 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
/src/zh/ @stevenyomi

View File

@ -1,5 +1,5 @@
name: 🐞 Issue report
description: Report a source issue in Tachiyomi
description: Report a source issue in Keiyoushi
labels: [Bug]
body:
@ -63,7 +63,7 @@ body:
description: |
You can find your Mihon/Tachiyomi version in **More → About**.
placeholder: |
Example: "0.16.3"
Example: "0.18.0"
validations:
required: true
@ -101,7 +101,7 @@ body:
required: true
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
- label: If this is an issue with the app itself, I should be opening an issue in the app repository.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -1,5 +1,5 @@
name: 🌐 Source request
description: Suggest a new source for Tachiyomi
description: Suggest a new source for Keiyoushi
labels: [Source request]
body:

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ apk/
gen
generated-src/
.kotlin
*.jks

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -38,7 +38,7 @@ kotlinter {
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
implementation(project(":utils"))
implementation(project(":core"))
}
tasks {

View File

@ -103,7 +103,6 @@ dependencies {
if (theme != null) implementation(theme) // Overrides core launcher icons
implementation(project(":core"))
compileOnly(libs.bundles.common)
implementation(project(":utils"))
}
tasks.register("writeManifestFile") {

View File

@ -1,5 +1,6 @@
plugins {
id("com.android.library")
kotlin("android")
}
android {
@ -9,17 +10,18 @@ android {
minSdk = AndroidConfig.minSdk
}
namespace = "eu.kanade.tachiyomi.extension.core"
sourceSets {
named("main") {
manifest.srcFile("AndroidManifest.xml")
res.setSrcDirs(listOf("res"))
}
}
namespace = "keiyoushi.core"
buildFeatures {
resValues = false
shaders = false
}
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
}
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
}

View File

@ -10,19 +10,19 @@ import uy.kohesive.injekt.injectLazy
val jsonInstance: Json by injectLazy()
/**
* Parses and serializes the String as the type <T>.
* Parses JSON string into an object of type [T].
*/
inline fun <reified T> String.parseAs(json: Json = jsonInstance): T =
json.decodeFromString(this)
/**
* Parse and serialize the response body as the type <T>.
* Parses the response body into an object of type [T].
*/
inline fun <reified T> Response.parseAs(json: Json = jsonInstance): T =
json.decodeFromStream(body.byteStream())
use { json.decodeFromStream(body.byteStream()) }
/**
* Serializes the object to a JSON String.
* Serializes the object to a JSON string.
*/
inline fun <reified T> T.toJsonString(json: Json = jsonInstance): String =
json.encodeToString(this)

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

4
gradlew generated vendored
View File

@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

188
gradlew.bat generated vendored
View File

@ -1,94 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 9
dependencies {
api(project(":lib:synchrony"))
}

View File

@ -1,8 +1,9 @@
<?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.slimereadtheme.SlimeReadThemeUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -11,11 +12,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/..*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

View File

@ -0,0 +1,247 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.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 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
abstract class Etoshore(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular ==============================
open val popularFilter = FilterList(
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest ===============================
open val latestFilter = FilterList(
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected().takeIf { it.value.isNotBlank() }
?: return@forEach
url.addQueryParameter(selected.query, selected.value)
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
.map { manga -> MangasPage(listOf(manga), false) }
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
setUrlWithoutDomain(element.absUrl("href"))
}
override fun searchMangaParse(response: Response): MangasPage {
if (filterList.isEmpty()) {
filterParse(response)
}
return super.searchMangaParse(response)
}
// ============================== Details ===============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst(".excerpt p")?.text()
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
.joinToString { it.text() }
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
?.text()
with(document) {
status = when {
containsClass(".finished") -> SManga.COMPLETED
containsClass(".publishing") -> SManga.ONGOING
containsClass(".on-hiatus") -> SManga.ON_HIATUS
containsClass(".discontinued") -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
private fun Element.containsClass(cssSelector: String) = select(cssSelector).isNotEmpty()
protected open fun imageFromElement(element: Element): String? {
val attributes = listOf(
"data-src",
"data-lazy-src",
"data-cfsrc",
"src",
)
return attributes
.mapNotNull { attr -> element.takeIf { it.hasAttr(attr) }?.attr("abs:$attr") }
.maxOrNull()
?: element.takeIf { it.hasAttr("srcset") }?.attr("abs:srcset")?.getSrcSetImage()
}
protected open fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
// ============================== Chapters ============================
override fun chapterListSelector() = ".chapter-list li a"
override fun chapterListParse(response: Response): List<SChapter> {
return super.chapterListParse(response)
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
Page(index, document.location(), imageFromElement(element))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================= Filters ==============================
private var filterList = emptyList<Pair<String, List<Tag>>>()
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (filterList.isNotEmpty()) {
filterList.map { SelectionList(it.first, it.second) }
} else {
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
}
return FilterList(filters)
}
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
val selectorFilter = "#filter-form $selector .select-item-head .text"
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
val tags = document.select("#filter-form $selector li").map { element ->
element.selectFirst("input")!!.let { input ->
Tag(
name = element.selectFirst(".text")!!.text(),
value = input.attr("value"),
query = input.attr("name"),
)
}
}
displayName to mutableListOf<Tag>().apply {
this += Tag("Default")
this += tags
}
}
}
open val filterListSelector: List<String> = listOf(
".filter-genre",
".filter-status",
".filter-type",
".filter-year",
".filter-sort",
)
open fun filterParse(response: Response) {
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
}
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
// ============================= Utils ==============================
private fun String.containsIn(array: Array<String>): Boolean {
return this.lowercase() in array.map { it.lowercase() }
}
companion object {
const val PREFIX_SEARCH = "id:"
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)$""".toRegex()
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class EtoshoreUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size >= 2) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${getSLUG(pathSegments)}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("EtoshoreUrl", e.toString())
}
} else {
Log.e("EtoshoreUrl", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
private fun getSLUG(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 2) {
val slug = pathSegments[1]
"${Etoshore.PREFIX_SEARCH}$slug"
} else {
null
}
}
}

View File

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

View File

@ -84,12 +84,14 @@ open class GoDa(
val document = response.asJsoup().selectFirst("main")!!
val titleElement = document.selectFirst("h1")!!
val elements = titleElement.parent()!!.parent()!!.children()
check(elements.size == 6)
check(elements[4].tagName() == "p")
title = titleElement.ownText()
status = when (titleElement.child(0).text()) {
"連載中", "Ongoing" -> SManga.ONGOING
"完結" -> SManga.COMPLETED
"停止更新" -> SManga.CANCELLED
"休刊" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })

View File

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

View File

@ -1,12 +1,13 @@
package eu.kanade.tachiyomi.multisrc.greenshit
import android.content.SharedPreferences
import android.util.Base64
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -15,83 +16,68 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferences
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
abstract class GreenShit(
override val name: String,
val url: String,
override val baseUrl: String,
override val lang: String,
val scanId: Long = 1,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
private val isCi = System.getenv("CI") == "true"
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences = getPreferences()
protected var apiUrl: String
get() = preferences.getString(API_BASE_URL_PREF, defaultApiUrl)!!
private set(value) = preferences.edit().putString(API_BASE_URL_PREF, value).apply()
private var restoreDefaultEnable: Boolean
get() = preferences.getBoolean(DEFAULT_PREF, false)
set(value) = preferences.edit().putBoolean(DEFAULT_PREF, value).apply()
override val baseUrl: String get() = when {
isCi -> defaultBaseUrl
else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
}
private val defaultBaseUrl: String = url
private val defaultApiUrl: String = "https://api.sussytoons.wtf"
protected open val apiUrl = "https://api.sussytoons.wtf"
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageLocation)
.build()
init {
if (restoreDefaultEnable) {
restoreDefaultEnable = false
preferences.edit().putString(DEFAULT_BASE_URL_PREF, null).apply()
preferences.edit().putString(API_DEFAULT_BASE_URL_PREF, null).apply()
}
open val targetAudience: TargetAudience = TargetAudience.All
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
preferences.getString(API_DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultApiUrl) {
preferences.edit()
.putString(API_BASE_URL_PREF, defaultApiUrl)
.putString(API_DEFAULT_BASE_URL_PREF, defaultApiUrl)
.apply()
}
}
}
open val contentOrigin: ContentOrigin = ContentOrigin.Web
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", scanId.toString())
// ============================= Popular ==================================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaRequest(page: Int) =
when (contentOrigin) {
ContentOrigin.Mobile -> GET("$apiUrl/obras/top5", headers)
else -> GET(baseUrl, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
override fun popularMangaParse(response: Response): MangasPage =
when (contentOrigin) {
ContentOrigin.Mobile -> popularMangaParseMobile(response)
else -> popularMangaParseWeb(response)
}
private fun popularMangaParseMobile(response: Response): MangasPage {
val mangas = response.parseAs<ResultDto<List<MangaDto>>>().toSMangaList()
return MangasPage(mangas, hasNextPage = false)
}
private fun popularMangaParseWeb(response: Response): MangasPage {
val json = response.parseScriptToJson().let(POPULAR_JSON_REGEX::find)
?.groups?.get(1)?.value
?: return MangasPage(emptyList(), false)
@ -105,11 +91,16 @@ abstract class GreenShit(
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.addQueryParameter("gen_id", "4")
.addQueryParameterIf(targetAudience != TargetAudience.All, "gen_id", targetAudience.toString())
.build()
return GET(url, headers)
}
private fun HttpUrl.Builder.addQueryParameterIf(predicate: Boolean, name: String, value: String): HttpUrl.Builder {
if (predicate) addQueryParameter(name, value)
return this
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
val mangas = dto.toSMangaList()
@ -134,19 +125,60 @@ abstract class GreenShit(
}
// ============================= Details ==================================
override fun getMangaUrl(manga: SManga) = when (contentOrigin) {
ContentOrigin.Mobile -> "$baseUrl${manga.url}"
else -> super.getMangaUrl(manga)
}
override fun mangaDetailsParse(response: Response): SManga {
override fun mangaDetailsRequest(manga: SManga): Request =
when (contentOrigin) {
ContentOrigin.Mobile -> mangaDetailsRequestMobile(manga)
else -> super.mangaDetailsRequest(manga)
}
private fun mangaDetailsRequestMobile(manga: SManga): Request {
val pathSegment = manga.url.substringBeforeLast("/").replace("obra", "obras")
return GET("$apiUrl$pathSegment", headers)
}
override fun mangaDetailsParse(response: Response) =
when (contentOrigin) {
ContentOrigin.Mobile -> response.parseAs<ResultDto<MangaDto>>().results.toSManga()
else -> mangaDetailsParseWeb(response)
}
private fun mangaDetailsParseWeb(response: Response): SManga {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?.groups?.get(1)?.value
?: throw IOException("Details do mangá não foi encontrado")
return json.parseAs<ResultDto<MangaDto>>().results.toSManga()
}
// ============================= Chapters =================================
override fun chapterListParse(response: Response): List<SChapter> {
override fun getChapterUrl(chapter: SChapter) = when (contentOrigin) {
ContentOrigin.Mobile -> "$baseUrl${chapter.url}"
else -> super.getChapterUrl(chapter)
}
override fun chapterListRequest(manga: SManga) =
when (contentOrigin) {
ContentOrigin.Mobile -> mangaDetailsRequest(manga)
else -> super.chapterListRequest(manga)
}
override fun chapterListParse(response: Response): List<SChapter> =
when (contentOrigin) {
ContentOrigin.Mobile -> chapterListParseMobile(response)
else -> chapterListParseWeb(response)
}.distinctBy(SChapter::url)
private fun chapterListParseMobile(response: Response): List<SChapter> =
response.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
private fun chapterListParseWeb(response: Response): List<SChapter> {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?.groups?.get(1)?.value
?: return emptyList()
return json.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
}
@ -155,7 +187,50 @@ abstract class GreenShit(
private val pageUrlSelector = "img.chakra-image"
override fun pageListParse(response: Response): List<Page> {
override fun pageListRequest(chapter: SChapter): Request =
when (contentOrigin) {
ContentOrigin.Mobile -> pageListRequestMobile(chapter)
else -> super.pageListRequest(chapter)
}
private fun pageListRequestMobile(chapter: SChapter): Request {
val pathSegment = chapter.url.replace("capitulo", "capitulo-app-token")
val newHeaders = headers.newBuilder()
.set("x-client-hash", generateToken(scanId, SECRET_KEY))
.set("authorization", "Bearer $token")
.build()
return GET("$apiUrl$pathSegment", newHeaders)
}
private fun generateToken(scanId: Long, secretKey: String): String {
val timestamp = System.currentTimeMillis() / 1000
val expiration = timestamp + 3600
val payload = buildJsonObject {
put("scan_id", scanId)
put("timestamp", timestamp)
put("exp", expiration)
}.toJsonString()
val hmac = Mac.getInstance("HmacSHA256")
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
hmac.init(secretKeySpec)
val signatureBytes = hmac.doFinal(payload.toByteArray())
val signature = signatureBytes.joinToString("") { "%02x".format(it) }
return Base64.encodeToString("$payload.$signature".toByteArray(), Base64.NO_WRAP)
}
override fun pageListParse(response: Response): List<Page> =
when (contentOrigin) {
ContentOrigin.Mobile -> pageListParseMobile(response)
else -> pageListParseWeb(response)
}
private fun pageListParseMobile(response: Response): List<Page> =
response.parseAs<ResultDto<ChapterPageDto>>().toPageList()
private fun pageListParseWeb(response: Response): List<Page> {
val document = response.asJsoup()
pageListParse(document).takeIf(List<Page>::isNotEmpty)?.let { return it }
@ -200,6 +275,57 @@ abstract class GreenShit(
return GET(page.url, imageHeaders)
}
// ============================= Login ========================================
private val credential: Credential by lazy {
Credential(
email = preferences.getString(USERNAME_PREF, "") as String,
password = preferences.getString(PASSWORD_PREF, "") as String,
)
}
private fun Token.save(): Token {
return this.also {
preferences.edit()
.putString(TOKEN_PREF, it.toJsonString())
.apply()
}
}
private var _cache: Token? = null
private val token: Token
get() {
if (_cache != null && _cache!!.isValid()) {
return _cache!!
}
val tokenValue = preferences.getString(TOKEN_PREF, Token().toJsonString())?.parseAs<Token>()
if (tokenValue != null && tokenValue.isValid()) {
return tokenValue.also { _cache = it }
}
return credential.takeIf(Credential::isNotEmpty)?.let(::doLogin)?.let { response ->
if (response.isSuccessful.not()) {
Token.empty().save()
throw IOException("Falha ao realizar o login")
}
val tokenDto = response.parseAs<ResultDto<TokenDto>>().results
Token(tokenDto.value).also {
_cache = it.save()
}
} ?: throw IOException("Adicione suas credenciais em Extensões > $name > Configurações")
}
val loginClient = network.cloudflareClient
fun doLogin(credential: Credential): Response {
val payload = buildJsonObject {
put("usr_email", credential.email)
put("usr_senha", credential.password)
}.toJsonString().toRequestBody("application/json".toMediaType())
return loginClient.newCall(POST("$apiUrl/me/login", headers, payload)).execute()
}
// ============================= Interceptors =================================
private fun imageLocation(chain: Interceptor.Chain): Response {
@ -224,52 +350,45 @@ abstract class GreenShit(
// ============================= Settings ====================================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val fields = listOf(
EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = URL_PREF_SUMMARY
if (contentOrigin != ContentOrigin.Mobile) {
return
}
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL padrão:\n$defaultBaseUrl"
val warning = "⚠️ Os dados inseridos nessa seção serão usados somente para realizar o login na fonte"
val message = "Insira %s para prosseguir com o acesso aos recursos disponíveis na fonte"
setDefaultValue(defaultBaseUrl)
},
EditTextPreference(screen.context).apply {
key = API_BASE_URL_PREF
title = API_BASE_URL_PREF_TITLE
summary = buildString {
append("Se não souber como verificar a URL da API, ")
append("busque suporte no Discord do repositório de extensões.")
appendLine(URL_PREF_SUMMARY)
append("\n⚠ A fonte não oferece suporte para essa extensão.")
}
EditTextPreference(screen.context).apply {
key = USERNAME_PREF
title = "📧 Email"
summary = "Email de acesso"
dialogMessage = buildString {
appendLine(message.format("seu email"))
append("\n$warning")
}
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL da API padrão:\n$defaultApiUrl"
setDefaultValue("")
setDefaultValue(defaultApiUrl)
},
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = DEFAULT_PREF
title = "Redefinir configurações"
summary = buildString {
append("Habilite para redefinir as configurações padrões no próximo reinicialização da aplicação.")
appendLine("Você pode limpar os dados da extensão em Configurações > Avançado:")
appendLine("\t - Limpar os cookies")
appendLine("\t - Limpar os dados da WebView")
appendLine("\t - Limpar o banco de dados (Procure a '$name' e remova os dados)")
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
},
)
EditTextPreference(screen.context).apply {
key = PASSWORD_PREF
title = "🔑 Senha"
summary = "Senha de acesso"
dialogMessage = buildString {
appendLine(message.format("sua senha"))
append("\n$warning")
}
setDefaultValue("")
fields.forEach(screen::addPreference)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
}
// ============================= Utilities ====================================
@ -299,24 +418,33 @@ abstract class GreenShit(
return this
}
enum class TargetAudience(val value: Int) {
All(1),
Shoujo(4),
Yaoi(7),
;
override fun toString() = value.toString()
}
enum class ContentOrigin {
Mobile,
Web,
}
companion object {
const val CDN_URL = "https://cdn.sussytoons.site"
val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex()
val POPULAR_JSON_REGEX = """(?:"dataTop":)(\{.+totalPaginas":\d+\})(?:.+"dataF)""".toRegex()
val DETAILS_CHAPTER_REGEX = """(\{\"resultado.+"\}{3})""".toRegex()
val DETAILS_CHAPTER_REGEX = """\{"obra":(\{.+"\}{3})""".toRegex()
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_TITLE = "Editar URL da fonte"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
private const val API_BASE_URL_PREF = "overrideApiUrl"
private const val API_BASE_URL_PREF_TITLE = "Editar URL da API da fonte"
private const val API_DEFAULT_BASE_URL_PREF = "defaultApiUrl"
private const val TOKEN_PREF = "greenShitToken"
private const val USERNAME_PREF = "usernamePref"
private const val PASSWORD_PREF = "passwordPref"
private const val DEFAULT_PREF = "defaultPref"
private const val SECRET_KEY = "sua_chave_secreta_aqui_32_caracteres"
}
}

View File

@ -13,8 +13,41 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import org.jsoup.Jsoup
import java.text.Normalizer
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@Serializable
class Token(
val value: String = "",
val updateAt: Long = Date().time,
) {
fun isValid() = value.isNotEmpty() && isExpired().not()
fun isExpired(): Boolean {
val updateAtDate = Date(updateAt)
val expiration = Calendar.getInstance().apply {
time = updateAtDate
add(Calendar.HOUR, 1)
}
return Date().after(expiration.time)
}
override fun toString() = value
companion object {
fun empty() = Token()
}
}
class Credential(
val email: String = "",
val password: String = "",
) {
fun isEmpty() = listOf(email, password).any(String::isBlank)
fun isNotEmpty() = isEmpty().not()
}
@Serializable
class ResultDto<T>(
@SerialName("pagina")
@ -84,6 +117,12 @@ class ResultDto<T>(
}
}
@Serializable
class TokenDto(
@SerialName("token")
val value: String,
)
@Serializable
class MangaDto(
@SerialName("obr_id")

View File

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

View File

@ -67,7 +67,19 @@ abstract class Iken(
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/api/posts".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
if (apiUrl.startsWith("https://api.", true)) {
addQueryParameter("tag", "latestUpdate")
addQueryParameter("isNovel", "false")
}
}.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {

View File

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

View File

@ -43,6 +43,8 @@ open class Kemono(
private val apiPath = "api/v1"
private val dataPath = "data"
private val imgCdnUrl = baseUrl.replace("//", "//img.")
private var mangasCache: List<KemonoCreatorDto> = emptyList()
@ -231,7 +233,7 @@ open class Kemono(
override fun pageListParse(response: Response): List<Page> {
val postData: KemonoPostDtoWrapped = response.parseAs()
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = "$baseUrl/$dataPath$path") }
}
override fun imageRequest(page: Page): Request {
@ -242,7 +244,7 @@ open class Kemono(
val index = imageUrl.indexOf('/', 8)
val url = buildString {
append(imageUrl, 0, index)
append("/thumbnail/data")
append("/thumbnail")
append(imageUrl.substring(index))
}
return GET(url, headers)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,193 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import kotlin.concurrent.thread
open class LectorMOnline(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/comics?sort=views&page=$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/comics?page=$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("comics")
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is SortByFilter -> {
if (filter.selected == "views") {
url.addQueryParameter("sort", "views")
}
if (filter.state!!.ascending) {
url.addQueryParameter("isDesc", "false")
}
}
is GenreFilter -> {
val selectedGenre = filter.toUriPart()
if (selectedGenre.isNotEmpty()) {
return GET("$baseUrl/genres/$selectedGenre?page=$page", headers)
}
}
else -> { }
}
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (response.request.url.pathSegments[0] == "genres") {
return searchMangaGenreParse(document)
}
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonData = COMICS_LIST_REGEX.find(script)?.groupValues?.get(1)?.unescape()
?: throw Exception("No se pudo encontrar la lista de cómics")
val data = jsonData.parseAs<ComicListDataDto>()
return MangasPage(data.comics.map { it.toSManga() }, data.hasNextPage())
}
private fun searchMangaGenreParse(document: Document): MangasPage {
val mangas = document.select("div.grid.relative > a.group.relative").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.attr("href").substringAfter("/comics/").substringBefore("?"))
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
}
val hasNextPage = document.selectFirst("div.flex.items-center > a:has(> svg):last-child:not(.pointer-events-none)") != null
return MangasPage(mangas, hasNextPage)
}
override fun getMangaUrl(manga: SManga) = "$baseUrl/comics/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/api/app/comic/${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<ComicDto>().toSMangaDetails()
}
override fun getChapterUrl(chapter: SChapter): String {
val mangaSlug = chapter.url.substringBefore("/")
val chapterNumber = chapter.url.substringAfter("/")
return "$baseUrl/comics/$mangaSlug/chapters/$chapterNumber"
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<ComicDto>().getChapters()
}
override fun pageListRequest(chapter: SChapter): Request {
val mangaSlug = chapter.url.substringBefore("/")
val chapterNumber = chapter.url.substringAfter("/")
return GET("$baseUrl/api/app/comic/$mangaSlug/chapter/$chapterNumber", headers)
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<ChapterPagesDataDto>()
return data.chapter.urlImagesChapter.mapIndexed { index, image ->
Page(index, imageUrl = image)
}
}
private var genresList: List<Pair<String, String>> = emptyList()
private var fetchFiltersAttempts = 0
private var filtersState = FiltersState.NOT_FETCHED
private fun fetchFilters() {
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
filtersState = FiltersState.FETCHING
fetchFiltersAttempts++
thread {
try {
val response = client.newCall(GET("$baseUrl/api/app/genres", headers)).execute()
val filters = response.parseAs<GenreListDto>()
genresList = filters.genres.map { genre -> genre.name.lowercase().replaceFirstChar { it.uppercase() } to genre.name }
filtersState = FiltersState.FETCHED
} catch (_: Throwable) {
filtersState = FiltersState.NOT_FETCHED
}
}
}
override fun getFilterList(): FilterList {
fetchFilters()
val filters = mutableListOf<Filter<*>>(
Filter.Header("El filtro por género no funciona con los demas filtros"),
Filter.Separator(),
SortByFilter(
"Ordenar por",
listOf(
SortProperty("Más vistos", "views"),
SortProperty("Más recientes", "created_at"),
),
1,
),
)
filters += if (filtersState == FiltersState.FETCHED) {
listOf(
Filter.Separator(),
Filter.Header("Filtrar por género"),
GenreFilter(genresList),
)
} else {
listOf(
Filter.Separator(),
Filter.Header("Presione 'Reiniciar' para intentar cargar los filtros"),
)
}
return FilterList(filters)
}
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
private fun String.unescape(): String {
return UNESCAPE_REGEX.replace(this, "$1")
}
companion object {
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
private val COMICS_LIST_REGEX = """\\"comicsData\\":(\{.*?\}),\\"searchParams""".toRegex()
}
}

View File

@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
class ComicListDataDto(
val comics: List<ComicDto>,
private val page: Int,
private val totalPages: Int,
) {
fun hasNextPage() = page < totalPages
}
@Serializable
class ComicDto(
private val slug: String,
private val name: String,
private val state: String?,
private val urlCover: String,
private val description: String?,
private val author: String?,
private val chapters: List<ChapterDto> = emptyList(),
) {
fun toSManga() = SManga.create().apply {
url = slug
title = name.substringBeforeLast("-").trim()
thumbnail_url = urlCover
status = state.parseStatus()
}
fun toSMangaDetails() = SManga.create().apply {
url = slug
title = name.substringBeforeLast("-").trim()
thumbnail_url = urlCover
description = this@ComicDto.description
status = state.parseStatus()
author = this@ComicDto.author
}
fun getChapters(): List<SChapter> {
return chapters.map { it.toSChapter(slug) }
}
private fun String?.parseStatus(): Int {
return when (this?.lowercase()) {
"ongoing" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@Serializable
class ChapterDto(
private val number: JsonPrimitive,
private val createdAt: String,
) {
fun toSChapter(mangaSlug: String) = SChapter.create().apply {
url = "$mangaSlug/$number"
name = "Capítulo $number"
date_upload = dateFormat.tryParse(createdAt)
}
}
@Serializable
class ChapterPagesDataDto(
val chapter: ChapterPagesDto,
)
@Serializable
class ChapterPagesDto(
val urlImagesChapter: List<String> = emptyList(),
)
@Serializable
class GenreListDto(
val genres: List<GenreDto>,
)
@Serializable
class GenreDto(
val name: String,
)

View File

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.source.model.Filter
class SortByFilter(title: String, private val sortProperties: List<SortProperty>, defaultIndex: Int) : Filter.Sort(
title,
sortProperties.map { it.name }.toTypedArray(),
Selection(defaultIndex, ascending = false),
) {
val selected: String
get() = sortProperties[state!!.index].value
}
class SortProperty(val name: String, val value: String) {
override fun toString(): String = name
}
class GenreFilter(genres: List<Pair<String, String>>) : UriPartFilter(
"Género",
arrayOf(
Pair("Todos", ""),
*genres.toTypedArray(),
),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}

View File

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

View File

@ -39,7 +39,22 @@ abstract class MadTheme(
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 1, TimeUnit.SECONDS)
.build()
.addInterceptor { chain ->
val request = chain.request()
val url = request.url
val response = chain.proceed(request)
if (!response.isSuccessful && url.fragment == "image-request") {
response.close()
val newUrl = url.newBuilder()
.host("sb.mbcdn.xyz")
.encodedPath(url.encodedPath.replaceFirst("/res/", "/"))
.fragment(null)
.build()
return@addInterceptor chain.proceed(request.newBuilder().url(newUrl).build())
}
response
}.build()
protected open val useLegacyApi = false
@ -329,6 +344,10 @@ abstract class MadTheme(
}
}
override fun imageRequest(page: Page): Request {
return GET("${page.imageUrl}#image-request", headers)
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()

View File

@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.multisrc.mccms
object Intl {
var lang = "zh"
val sort
get() = when (lang) {
"zh" -> "排序"
else -> "Sort by"
}
val popular
get() = when (lang) {
"zh" -> "热门人气"
else -> "Popular"
}
val latest
get() = when (lang) {
"zh" -> "更新时间"
else -> "Latest"
}
val score
get() = when (lang) {
"zh" -> "评分"
else -> "Score"
}
val status
get() = when (lang) {
"zh" -> "进度"
else -> "Status"
}
val all
get() = when (lang) {
"zh" -> "全部"
else -> "All"
}
val ongoing
get() = when (lang) {
"zh" -> "连载"
else -> "Ongoing"
}
val completed
get() = when (lang) {
"zh" -> "完结"
else -> "Completed"
}
val genreWeb
get() = when (lang) {
"zh" -> "标签"
else -> "Genre"
}
val genreApi
get() = when (lang) {
"zh" -> "标签(搜索文本时无效)"
else -> "Genre (ignored for text search)"
}
val categoryWeb
get() = when (lang) {
"zh" -> "分类筛选(搜索时无效)"
else -> "Category filters (ignored for text search)"
}
val tapReset
get() = when (lang) {
"zh" -> "点击“重置”尝试刷新标签分类"
else -> "Tap 'Reset' to load genres"
}
}

View File

@ -9,15 +9,13 @@ 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.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import keiyoushi.utils.parseAs as parseAsRaw
/**
* 漫城CMS http://mccms.cn/
@ -25,16 +23,26 @@ import java.net.URLEncoder
open class MCCMS(
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
final override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest = true
override val supportsLatest get() = true
private val json: Json by injectLazy()
init {
Intl.lang = lang
}
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain -> // for thumbnail requests
var request = chain.request()
val referer = request.header("Referer")
if (referer != null && !request.url.toString().startsWith(referer)) {
request = request.newBuilder().removeHeader("Referer").build()
}
chain.proceed(request)
}
.build()
}
@ -42,12 +50,14 @@ open class MCCMS(
.add("User-Agent", System.getProperty("http.agent")!!)
.add("Referer", baseUrl)
protected open fun SManga.cleanup(): SManga = this
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
override fun popularMangaParse(response: Response): MangasPage {
val list: List<MangaDto> = response.parseAs()
return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE)
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
}
override fun latestUpdatesRequest(page: Int): Request =
@ -86,7 +96,7 @@ open class MCCMS(
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val list = response.parseAs<List<MangaDto>>()
list.first { it.cleanUrl == mangaUrl }.toSManga()
list.first { it.cleanUrl == mangaUrl }.toSManga().cleanup()
}
}
@ -120,9 +130,7 @@ open class MCCMS(
// Don't send referer
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
}
private inline fun <reified T> Response.parseAs(): T = parseAsRaw<ResultDto<T>>().data
override fun getFilterList(): FilterList {
val genreData = config.genreData.also { it.fetchGenres(this) }

View File

@ -12,6 +12,8 @@ val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; W
fun String.removePathPrefix() = removePrefix("/index.php")
fun String.mobileUrl() = replace("//www.", "//m.")
open class MCCMSConfig(
hasCategoryPage: Boolean = true,
val textSearchOnlyPageOne: Boolean = false,

View File

@ -26,11 +26,11 @@ data class MangaDto(
title = Entities.unescape(name)
author = Entities.unescape(this@MangaDto.author)
description = Entities.unescape(content)
genre = tags.joinToString()
status = when {
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
'完' in serialize -> SManga.COMPLETED
else -> SManga.UNKNOWN
genre = Entities.unescape(tags.joinToString())
status = when (serialize) {
"连载", "連載中", "En cours", "OnGoing" -> SManga.ONGOING
"完结", "已完結", "Terminé", "Complete", "Complété" -> SManga.COMPLETED
else -> if (isUpdating(addtime)) SManga.ONGOING else SManga.UNKNOWN
}
thumbnail_url = "$pic#$id"
initialized = true

View File

@ -18,32 +18,31 @@ open class MCCMSFilter(
val query get() = queries[state]
}
class SortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES_WEB)
class SortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES_WEB)
private val SORT_NAMES = arrayOf("热门人气", "更新时间", "评分")
private val SORT_QUERIES = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB = arrayOf("order/hits", "order/addtime", "order/score")
private val SORT_NAMES get() = arrayOf(Intl.popular, Intl.latest, Intl.score)
private val SORT_QUERIES get() = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB get() = arrayOf("order/hits", "order/addtime", "order/score")
class StatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES_WEB)
class StatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES_WEB)
private val STATUS_NAMES = arrayOf("全部", "连载", "完结")
private val STATUS_QUERIES = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB = arrayOf("", "finish/1", "finish/2")
private val STATUS_NAMES get() = arrayOf(Intl.all, Intl.ongoing, Intl.completed)
private val STATUS_QUERIES get() = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB get() = arrayOf("", "finish/1", "finish/2")
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
private val apiQueries get() = queries.run {
Array(size) { i -> "type[tags]=" + this[i] }
Array(size) { i -> "type[tags]=" + this[i] }.apply { this[0] = "" }
}
private val webQueries get() = queries.run {
Array(size) { i -> "tags/" + this[i] }
Array(size) { i -> "tags/" + this[i] }.apply { this[0] = "" }
}
val filter get() = MCCMSFilter("标签(搜索文本时无效)", values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter("标签", values, webQueries, isTypeQuery = true)
val filter get() = MCCMSFilter(Intl.genreApi, values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter(Intl.genreWeb, values, webQueries, isTypeQuery = true)
}
class GenreData(hasCategoryPage: Boolean) {
@ -55,7 +54,12 @@ class GenreData(hasCategoryPage: Boolean) {
status = FETCHING
thread {
try {
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
val request = when (source) {
// Web sources parse listings whenever possible. They call this function for mobile pages.
is MCCMSWeb -> GET("${source.baseUrl.mobileUrl()}/category/", source.headers)
else -> GET("${source.baseUrl}/category/", pcHeaders)
}
val response = source.client.newCall(request).execute()
parseGenres(response.asJsoup(), this)
} catch (e: Exception) {
status = NOT_FETCHED
@ -74,7 +78,7 @@ class GenreData(hasCategoryPage: Boolean) {
internal fun parseGenres(document: Document, genreData: GenreData) {
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
val box = document.selectFirst(".cate-selector, .cy_list_l")
val box = document.selectFirst(".cate-selector, .cy_list_l, .ticai, .stui-screen__list")
if (box == null || "/tags/" in document.location()) {
genreData.status = GenreData.NOT_FETCHED
return
@ -85,7 +89,7 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
return
}
val result = buildList(genres.size + 1) {
add(Pair("全部", ""))
add(Pair(Intl.all, ""))
genres.mapTo(this) {
val tagId = it.attr("href").substringAfterLast('/')
Pair(it.text(), tagId)
@ -100,14 +104,14 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
internal fun getFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
add(StatusFilter())
if (Intl.lang == "zh") add(StatusFilter())
add(SortFilter())
if (genreData.status == GenreData.NO_DATA) return@buildList
add(Filter.Separator())
if (genreData.status == GenreData.FETCHED) {
add(genreData.genreFilter.filter)
} else {
add(Filter.Header("点击“重置”尝试刷新标签分类"))
add(Filter.Header(Intl.tapReset))
}
}
return FilterList(list)
@ -115,13 +119,13 @@ internal fun getFilters(genreData: GenreData): FilterList {
internal fun getWebFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
add(Filter.Header("分类筛选(搜索时无效)"))
add(Filter.Header(Intl.categoryWeb))
add(WebStatusFilter())
add(WebSortFilter())
when (genreData.status) {
GenreData.NO_DATA -> return@buildList
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
else -> add(Filter.Header("点击“重置”尝试刷新标签分类"))
else -> add(Filter.Header(Intl.tapReset))
}
}
return FilterList(list)

View File

@ -13,39 +13,45 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
open class MCCMSWeb(
override val name: String,
override val baseUrl: String,
override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
final override val lang: String = "zh",
protected val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest get() = true
init {
Intl.lang = lang
}
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.request.url.encodedPath == "/err/comic") {
throw IOException(response.body.string().substringBefore('\n'))
}
response
}
.build()
}
override fun headersBuilder() = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)
private fun parseListing(document: Document): MangasPage {
open fun parseListing(document: Document): MangasPage {
parseGenres(document, config.genreData)
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
}
val mangas = document.select(simpleMangaSelector()).map(::simpleMangaFromElement)
val hasNextPage = run { // default pagination
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
val buttons = document.selectFirst("#Pagination, .NewPages")!!.select(Evaluator.Tag("a"))
val count = buttons.size
// Next page != Last page
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
@ -53,6 +59,15 @@ open class MCCMSWeb(
return MangasPage(mangas, hasNextPage)
}
open fun simpleMangaSelector() = ".common-comic-item"
open fun simpleMangaFromElement(element: Element) = SManga.create().apply {
val titleElement = element.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
@ -104,6 +119,8 @@ open class MCCMSWeb(
return super.fetchMangaDetails(manga)
}
override fun getMangaUrl(manga: SManga) = baseUrl.mobileUrl() + manga.url
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun mangaDetailsParse(response: Response): SManga {
@ -127,17 +144,23 @@ open class MCCMSWeb(
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun chapterListParse(response: Response): List<SChapter> {
return run {
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
return getDescendingChapters(
response.asJsoup().select(chapterListSelector()).map {
val link = it.child(0)
SChapter.create().apply {
url = link.attr("href").removePathPrefix()
name = link.ownText()
name = link.text()
}
}.asReversed()
}
},
)
}
open fun chapterListSelector() = ".chapter__list-box > li"
open fun getDescendingChapters(chapters: List<SChapter>) = chapters.asReversed()
override fun getChapterUrl(chapter: SChapter) = baseUrl.mobileUrl() + chapter.url
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)

View File

@ -0,0 +1,9 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1
dependencies {
implementation(project(":lib:unpacker"))
}

View File

@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.Serializable
@Serializable
class ResponseDto(val data: List<ChapterDto>)
@Serializable
class ChapterDto(
private val chapterid: String,
private val chaptername: String,
) {
fun toSChapter(mangaId: String) = SChapter.create().apply {
url = "$mangaId/$chapterid"
name = chaptername
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.source.model.Filter
class Option(val name: String, val value: String)
open class SelectFilter(name: String, val options: Array<Option>) :
Filter.Select<String>(name, Array(options.size) { options[it].name })
class RankingFilter : SelectFilter(
"排行榜",
arrayOf(
Option("不查看", ""),
Option("精品榜", "1"),
Option("人气榜", "2"),
Option("推荐榜", "3"),
Option("黑马榜", "4"),
Option("最近更新", "5"),
Option("新漫画", "6"),
),
)
class CategoryFilter : SelectFilter(
"分类",
arrayOf(
Option("全部", ""),
Option("冒险", "1"),
Option("热血", "2"),
Option("都市", "3"),
Option("玄幻", "4"),
Option("悬疑", "5"),
Option("耽美", "6"),
Option("恋爱", "7"),
Option("生活", "8"),
Option("搞笑", "9"),
Option("穿越", "10"),
Option("修真", "11"),
Option("后宫", "12"),
Option("女主", "13"),
Option("古风", "14"),
Option("连载", "15"),
Option("完结", "16"),
),
)

View File

@ -0,0 +1,200 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
// Rumanhua legacy preference:
// const val APP_CUSTOMIZATION_URL = "APP_CUSTOMIZATION_URL"
/** 漫漫看 */
open class MMLook(
override val name: String,
override val baseUrl: String,
private val desktopUrl: String,
private val useLegacyMangaUrl: Boolean,
) : HttpSource() {
override val lang: String get() = "zh"
override val supportsLatest: Boolean get() = true
override val client = network.cloudflareClient.newBuilder()
.followRedirects(false)
.hostnameVerifier { _, _ -> true }
.build()
private fun String.certificateWorkaround() = replace("https:", "http:")
private fun SManga.formatUrl() = apply { if (useLegacyMangaUrl) url = "/$url/" }
private fun rankingRequest(id: String) = GET("$desktopUrl/rank/$id", headers)
override fun popularMangaRequest(page: Int) = rankingRequest("1")
override fun popularMangaParse(response: Response): MangasPage {
val entries = response.asJsoup().select(".likedata").map { element ->
SManga.create().apply {
url = element.select("a").attr("href").mustRemoveSurrounding("/", "/")
title = element.selectFirst(".le-t")!!.text()
author = element.selectFirst(".likeinfo > p")!!.text()
.mustRemoveSurrounding("作者:", "")
description = element.selectFirst(".le-j")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}.formatUrl()
}
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int) = rankingRequest("5")
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun getFilterList() = FilterList(
RankingFilter(),
Filter.Separator(),
Filter.Header("分类(搜索文本、查看排行榜时无效)"),
CategoryFilter(),
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
return POST(
"$desktopUrl/s",
headers,
FormBody.Builder().add("k", query.take(12)).build(),
)
}
for (filter in filters) {
when (filter) {
is RankingFilter -> if (filter.state > 0) {
return rankingRequest(filter.options[filter.state].value)
}
is CategoryFilter -> if (filter.state > 0) {
val id = filter.options[filter.state].value
return GET("$desktopUrl/sort/$id", headers)
}
else -> {}
}
}
return popularMangaRequest(page)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.method == "GET") return popularMangaParse(response)
val entries = response.asJsoup().select(".col-auto").map { element ->
SManga.create().apply {
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
title = element.selectFirst(".e-title")!!.text()
author = element.selectFirst(".tip")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}.formatUrl()
}
return MangasPage(entries, false)
}
override fun getMangaUrl(manga: SManga): String {
val id = manga.url.removeSurrounding("/")
return "$baseUrl/$id/".certificateWorkaround()
}
// Desktop page has consistent template and more initial chapters
override fun mangaDetailsRequest(manga: SManga): Request {
val id = manga.url.removeSurrounding("/")
return GET("$desktopUrl/$id/", headers)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val comicInfo = response.asJsoup().selectFirst(".comicInfo")!!
thumbnail_url = comicInfo.selectFirst("img")!!.attr("data-src")
val container = comicInfo.selectFirst(".detinfo")!!
title = container.selectFirst("h1")!!.text()
var updated = ""
for (span in container.select("span")) {
val text = span.ownText()
val value = text.substring(4).trimStart()
when (val key = text.substring(0, 4)) {
"作 者:" -> author = value
"更新时间" -> updated = "$text\n\n"
"标 签:" -> genre = value.replace(" ", ", ")
"状 态:" -> status = when (value) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
else -> throw Exception("Unknown field: $key")
}
}
description = updated + container.selectFirst(".content")!!.text()
}
// Desktop page contains more initial chapters
// "more chapter" request must be sent to the same domain
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val container = response.asJsoup().selectFirst(".chapterlistload")!!
val chapters = container.child(0).children().mapTo(ArrayList()) { element ->
SChapter.create().apply {
url = element.attr("href").mustRemoveSurrounding("/", ".html")
name = element.text()
}
}
if (container.selectFirst(".chaplist-more") != null) {
val mangaId = response.request.url.pathSegments[0]
val request = POST(
"$desktopUrl/morechapter",
headers,
FormBody.Builder().addEncoded("id", mangaId).build(),
)
client.newCall(request).execute().parseAs<ResponseDto>().data
.mapTo(chapters) { it.toSChapter(mangaId) }
}
return chapters
}
private fun SChapter.fullUrl(): String {
val url = this.url
if (url.startsWith('/')) throw Exception("请刷新章节列表")
return "$baseUrl/$url.html"
}
override fun getChapterUrl(chapter: SChapter) = chapter.fullUrl().certificateWorkaround()
override fun pageListRequest(chapter: SChapter): Request = GET(chapter.fullUrl(), headers)
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val id = document.selectFirst(".readerContainer")!!.attr("data-id").toInt()
return document.selectFirst("script:containsData(eval)")!!.data()
.let(Unpacker::unpack)
.mustRemoveSurrounding("var __c0rst96=\"", "\"")
.let { decrypt(it, id) }
.parseAs<List<String>>()
.mapIndexed { i, imageUrl -> Page(i, imageUrl = imageUrl) }
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}
private fun String.mustRemoveSurrounding(prefix: String, suffix: String): String {
check(startsWith(prefix) && endsWith(suffix)) { "string doesn't match $prefix[...]$suffix" }
return substring(prefix.length, length - suffix.length)
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import android.util.Base64
import kotlin.experimental.xor
// all2.js?v=2.3
fun decrypt(data: String, index: Int): String {
val key = when (index) {
0 -> "smkhy258"
1 -> "smkd95fv"
2 -> "md496952"
3 -> "cdcsdwq"
4 -> "vbfsa256"
5 -> "cawf151c"
6 -> "cd56cvda"
7 -> "8kihnt9"
8 -> "dso15tlo"
9 -> "5ko6plhy"
else -> throw Exception("Unknown index: $index")
}.encodeToByteArray()
val keyLength = key.size
val bytes = Base64.decode(data, Base64.DEFAULT)
for (i in bytes.indices) {
bytes[i] = bytes[i] xor key[i % keyLength]
}
return String(Base64.decode(bytes, Base64.DEFAULT))
}

View File

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

View File

@ -95,7 +95,7 @@ abstract class PizzaReader(
artist = comic.artist
description = comic.description
genre = comic.genres.joinToString(", ") { it.name }
status = comic.status.toStatus()
status = comic.status?.toStatus() ?: SManga.UNKNOWN
thumbnail_url = comic.thumbnail
}

View File

@ -27,7 +27,7 @@ data class PizzaComicDto(
val description: String = "",
val genres: List<PizzaGenreDto> = emptyList(),
@SerialName("last_chapter") val lastChapter: PizzaChapterDto? = null,
val status: String = "",
val status: String? = null,
val title: String = "",
val thumbnail: String = "",
val url: String = "",

View File

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

View File

@ -431,7 +431,7 @@ abstract class Senkuro(
companion object {
private const val offsetCount = 20
private const val API_URL = "https://api.senkuro.com/graphql"
private const val API_URL = "https://api.senkuro.me/graphql"
private val senkuroExcludeGenres = listOf("hentai", "yaoi", "yuri", "shoujo_ai", "shounen_ai")
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,273 +0,0 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.ChapterDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.LatestResponseDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.MangaInfoDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PageListDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PopularMangaDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.toSMangaList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.min
abstract class SlimeReadTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val scanId: String = "",
) : HttpSource() {
protected open val apiUrl: String by lazy { getApiUrlFromPage() }
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
protected open val urlInfix: String = "slimeread.com"
protected open fun getApiUrlFromPage(): String {
val initClient = network.cloudflareClient
val response = initClient.newCall(GET(baseUrl, headers)).execute()
if (!response.isSuccessful) throw Exception("HTTP error ${response.code}")
val document = response.asJsoup()
val scriptUrl = document.selectFirst("script[src*=pages/_app]")?.attr("abs:src")
?: throw Exception("Could not find script URL")
val scriptResponse = initClient.newCall(GET(scriptUrl, headers)).execute()
if (!scriptResponse.isSuccessful) throw Exception("HTTP error ${scriptResponse.code}")
val script = scriptResponse.body.string()
val apiUrl = FUNCTION_REGEX.find(script)?.let { result ->
val varBlock = result.groupValues[1]
val varUrlInfix = result.groupValues[2]
val block = """${varBlock.replace(varUrlInfix, "\"$urlInfix\"")}.toString()"""
try {
QuickJs.create().use { it.evaluate(block) as String }
} catch (e: Exception) {
null
}
}
return apiUrl?.let { "https://$it" } ?: throw Exception("Could not find API URL")
}
// ============================== Popular ===============================
private var popularMangeCache: MangasPage? = null
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/book_search?order=1&status=0".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
popularMangeCache = popularMangeCache?.takeIf { page != 1 }
?: super.fetchPopularManga(page).toBlocking().last()
return pageableOf(page, popularMangeCache!!)
}
override fun popularMangaParse(response: Response): MangasPage {
val items = response.parseAs<List<PopularMangaDto>>()
val mangaList = items.toSMangaList()
return MangasPage(mangaList, mangaList.isNotEmpty())
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/books?page=$page".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<LatestResponseDto>()
val mangaList = dto.data.toSMangaList()
val hasNextPage = dto.page < dto.pages
return MangasPage(mangaList, hasNextPage)
}
// =============================== Search ===============================
private var searchMangaCache: MangasPage? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$apiUrl/book/$id", headers))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
searchMangaCache = searchMangaCache?.takeIf { page != 1 }
?: super.fetchSearchManga(page, query, filters).toBlocking().last()
pageableOf(page, searchMangaCache!!)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response)
return MangasPage(listOf(details), false)
}
override fun getFilterList() = SlimeReadThemeFilters.FILTER_LIST
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val params = SlimeReadThemeFilters.getSearchParameters(filters)
val url = "$apiUrl/book_search".toHttpUrl().newBuilder()
.addIfNotBlank("query", query)
.addIfNotBlank("genre[]", params.genre)
.addIfNotBlank("status", params.status)
.addIfNotBlank("searchMethod", params.searchMethod)
.addIfNotBlank("scan_id", scanId)
.apply {
params.categories.forEach {
addQueryParameter("categories[]", it)
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.replace("/book/", "/manga/")
override fun mangaDetailsRequest(manga: SManga) = GET(apiUrl + manga.url, headers)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val info = response.parseAs<MangaInfoDto>()
thumbnail_url = info.thumbnail_url
title = info.name
description = info.description
genre = info.categories.joinToString()
url = "/book/${info.id}"
status = when (info.status) {
1 -> SManga.ONGOING
2 -> SManga.COMPLETED
3, 4 -> SManga.CANCELLED
5 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga) =
GET("$apiUrl/book_cap_units_all?manga_id=${manga.url.substringAfterLast("/")}", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val items = response.parseAs<List<ChapterDto>>()
val mangaId = response.request.url.queryParameter("manga_id")!!
return items.map {
SChapter.create().apply {
name = "Cap " + parseChapterNumber(it.number)
date_upload = parseChapterDate(it.updated_at)
chapter_number = it.number
scanlator = it.scan?.scan_name
url = "/book_cap_units?manga_id=$mangaId&cap=${it.number}"
}
}.reversed()
}
private fun parseChapterNumber(number: Float): String {
val cap = number + 1F
return "%.2f".format(cap)
.let { if (cap < 10F) "0$it" else it }
.replace(",00", "")
.replace(",", ".")
}
private fun parseChapterDate(date: String): Long {
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { 0L }
}
override fun getChapterUrl(chapter: SChapter): String {
val url = "$baseUrl${chapter.url}".toHttpUrl()
val id = url.queryParameter("manga_id")!!
val cap = url.queryParameter("cap")!!.toFloat()
val num = parseChapterNumber(cap)
return "$baseUrl/ler/$id/cap-$num"
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter) = GET(apiUrl + chapter.url, headers)
override fun pageListParse(response: Response): List<Page> {
val body = response.body.string()
val pages = if (body.startsWith("{")) {
json.decodeFromString<Map<String, PageListDto>>(body).values.flatMap { it.pages }
} else {
json.decodeFromString<List<PageListDto>>(body).flatMap { it.pages }
}
return pages.mapIndexed { index, item ->
Page(index, "", item.url)
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
/**
* Handles a large manga list and returns a paginated response.
* The app can't handle the large JSON list without pagination.
*
* @param page The page number to retrieve.
* @param cache The cached manga page containing the full list of mangas.
*/
private fun pageableOf(page: Int, cache: MangasPage) = Observable.just(cache).map { mangaPage ->
val mangas = mangaPage.mangas
val pageSize = 15
val currentSlice = (page - 1) * pageSize
val startIndex = min(mangas.size, currentSlice)
val endIndex = min(mangas.size, currentSlice + pageSize)
val slice = mangas.subList(startIndex, endIndex)
MangasPage(slice, hasNextPage = endIndex < mangas.size)
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
if (value.isNotBlank()) addQueryParameter(query, value)
return this
}
companion object {
const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -1,141 +0,0 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
object SlimeReadThemeFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : Filter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
val selected get() = vals[state].second
}
private inline fun <reified R> FilterList.getSelected(): String {
return (first { it is R } as SelectFilter).selected
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
Filter.Group<Filter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
private class CheckBoxVal(name: String) : Filter.CheckBox(name, false)
private inline fun <reified R> FilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): Sequence<String> {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
}
internal class CategoriesFilter : CheckBoxFilterList("Categorias", SlimeReadFiltersData.CATEGORIES)
internal class GenreFilter : SelectFilter("Gênero", SlimeReadFiltersData.GENRES)
internal class SearchMethodFilter : SelectFilter("Método de busca", SlimeReadFiltersData.SEARCH_METHODS)
internal class StatusFilter : SelectFilter("Status", SlimeReadFiltersData.STATUS)
val FILTER_LIST get() = FilterList(
CategoriesFilter(),
GenreFilter(),
SearchMethodFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val categories: Sequence<String> = emptySequence(),
val genre: String = "",
val searchMethod: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: FilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<CategoriesFilter>(SlimeReadFiltersData.CATEGORIES),
filters.getSelected<GenreFilter>(),
filters.getSelected<SearchMethodFilter>(),
filters.getSelected<StatusFilter>(),
)
}
private object SlimeReadFiltersData {
val CATEGORIES = arrayOf(
Pair("Adulto", "125"),
Pair("Artes Marciais", "117"),
Pair("Avant Garde", "154"),
Pair("Aventura", "112"),
Pair("Ação", "146"),
Pair("Comédia", "147"),
Pair("Culinária", "126"),
Pair("Doujinshi", "113"),
Pair("Drama", "148"),
Pair("Ecchi", "127"),
Pair("Erotico", "152"),
Pair("Esporte", "135"),
Pair("Fantasia", "114"),
Pair("Ficção Científica", "120"),
Pair("Filosofico", "150"),
Pair("Harém", "128"),
Pair("Histórico", "115"),
Pair("Isekai", "129"),
Pair("Josei", "116"),
Pair("Mecha", "130"),
Pair("Militar", "149"),
Pair("Mistério", "142"),
Pair("Médico", "118"),
Pair("One-shot", "131"),
Pair("Premiado", "155"),
Pair("Psicológico", "119"),
Pair("Romance", "141"),
Pair("Seinen", "140"),
Pair("Shoujo", "133"),
Pair("Shoujo-ai", "121"),
Pair("Shounen", "139"),
Pair("Shounen-ai", "134"),
Pair("Slice-of-life", "122"),
Pair("Sobrenatural", "123"),
Pair("Sugestivo", "153"),
Pair("Terror", "144"),
Pair("Thriller", "151"),
Pair("Tragédia", "137"),
Pair("Vida Escolar", "132"),
Pair("Yaoi", "124"),
Pair("Yuri", "136"),
)
private val SELECT = Pair("Selecione", "")
val GENRES = arrayOf(
SELECT,
Pair("Manga", "29"),
Pair("Light Novel", "34"),
Pair("Manhua", "31"),
Pair("Manhwa", "30"),
Pair("Novel", "33"),
Pair("Webcomic", "35"),
Pair("Webnovel", "36"),
Pair("Webtoon", "32"),
Pair("4-Koma", "37"),
)
val SEARCH_METHODS = arrayOf(
SELECT,
Pair("Preciso", "0"),
Pair("Geral", "1"),
)
val STATUS = arrayOf(
SELECT,
Pair("Em andamento", "1"),
Pair("Completo", "2"),
Pair("Dropado", "3"),
Pair("Cancelado", "4"),
Pair("Hiato", "5"),
)
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.slimeread.dto
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PopularMangaDto(
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_id") val id: Int,
@SerialName("book_name_original") val name: String,
)
@Serializable
data class LatestResponseDto(
val pages: Int,
val page: Int,
val data: List<PopularMangaDto>,
)
fun List<PopularMangaDto>.toSMangaList(): List<SManga> = map { item ->
SManga.create().apply {
thumbnail_url = item.thumbnail_url
title = item.name
url = "/book/${item.id}"
}
}
@Serializable
data class MangaInfoDto(
@SerialName("book_id") val id: Int,
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_name_original") val name: String,
@SerialName("book_status") val status: Int,
@SerialName("book_synopsis") val description: String?,
@SerialName("book_categories") private val _categories: List<CategoryDto>,
) {
@Serializable
data class CategoryDto(val categories: CatDto)
@Serializable
data class CatDto(@SerialName("cat_name_ptBR") val name: String)
val categories = _categories.map { it.categories.name }
}
@Serializable
data class ChapterDto(
@SerialName("btc_cap") val number: Float,
@SerialName("btc_date_updated") val updated_at: String,
val scan: ScanDto?,
) {
@Serializable
data class ScanDto(val scan_name: String?)
}
@Serializable
data class PageListDto(@SerialName("book_temp_cap_unit") val pages: List<PageDto>)
@Serializable
data class PageDto(
@SerialName("btcu_image") private val path: String,
@SerialName("btcu_provider_host") private val hostId: Int?,
) {
val url by lazy {
val baseUrl = when (hostId) {
2 -> "https://cdn.slimeread.com/"
5 -> "https://black.slimeread.com/"
else -> "https://objects.slimeread.com/"
}
baseUrl + path
}
}

View File

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

View File

@ -118,8 +118,9 @@ abstract class VerComics(
protected open val pageListSelector =
"div.wp-content p > img:not(noscript img), " +
"div.wp-content div#lector > img:not(noscript img), " +
"div.wp-content > figure img:not(noscript img)," +
"div.wp-content > img, div.wp-content > p img"
"div.wp-content > figure img:not(noscript img), " +
"div.wp-content > img, div.wp-content > p img, " +
"div.post-imgs > img"
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector)
.mapIndexed { i, img -> Page(i, imageUrl = img.imgAttr()) }

View File

@ -1,308 +0,0 @@
package eu.kanade.tachiyomi.multisrc.webtoons
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter.Header
import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Separator
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.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.SocketException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
open class Webtoons(
override val name: String,
override val baseUrl: String,
override val lang: String,
open val langCode: String = lang,
open val localeForCookie: String = lang,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH),
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return listOf<Cookie>(
Cookie.Builder()
.domain("www.webtoons.com")
.path("/")
.name("ageGatePass")
.value("true")
.name("locale")
.value(localeForCookie)
.name("needGDPR")
.value("false")
.build(),
)
}
},
)
.addInterceptor(::sslRetryInterceptor)
.build()
// m.webtoons.com throws an SSL error that can be solved by a simple retry
private fun sslRetryInterceptor(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: SocketException) {
chain.proceed(chain.request())
}
}
private val day: String
get() {
return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
Calendar.SUNDAY -> "div._list_SUNDAY"
Calendar.MONDAY -> "div._list_MONDAY"
Calendar.TUESDAY -> "div._list_TUESDAY"
Calendar.WEDNESDAY -> "div._list_WEDNESDAY"
Calendar.THURSDAY -> "div._list_THURSDAY"
Calendar.FRIDAY -> "div._list_FRIDAY"
Calendar.SATURDAY -> "div._list_SATURDAY"
else -> {
"div"
}
}
}
val json: Json by injectLazy()
override fun popularMangaSelector() = "not using"
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "https://www.webtoons.com/$langCode/")
protected val mobileHeaders: Headers = super.headersBuilder()
.add("Referer", "https://m.webtoons.com")
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
override fun popularMangaParse(response: Response): MangasPage {
val mangas = mutableListOf<SManga>()
val document = response.asJsoup()
var maxChild = 0
// For ongoing webtoons rows are ordered by descending popularity, count how many rows there are
document.select("div#dailyList .daily_section").forEach { day ->
day.select("li").count().let { rowCount ->
if (rowCount > maxChild) maxChild = rowCount
}
}
// Process each row
for (i in 1..maxChild) {
document.select("div#dailyList .daily_section li:nth-child($i) a").map { mangas.add(popularMangaFromElement(it)) }
}
// Add completed webtoons, no sorting needed
document.select("div.daily_lst.comp li a").map { mangas.add(popularMangaFromElement(it)) }
return MangasPage(mangas.distinctBy { it.url }, false)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.setUrlWithoutDomain(element.attr("href"))
manga.title = element.select("p.subj").text()
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesNextPageSelector(): String? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (!query.startsWith(URL_SEARCH_PREFIX)) {
return super.fetchSearchManga(page, query, filters)
}
val emptyResult = Observable.just(MangasPage(emptyList(), false))
// given a url to either a webtoon or an episode, returns a url path to corresponding webtoon
fun webtoonPath(u: HttpUrl) = when {
langCode == u.pathSegments[0] -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/${u.pathSegments[2]}/list"
else -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/list" // dongmanmanhua doesn't include langCode
}
return query.substringAfter(URL_SEARCH_PREFIX).toHttpUrlOrNull()?.let { url ->
val title_no = url.queryParameter("title_no")
val couldBeWebtoonOrEpisode = title_no != null && (url.pathSegments.size >= 3 && url.pathSegments.last().isNotEmpty())
val isThisLang = "$url".startsWith("$baseUrl/$langCode")
if (!(couldBeWebtoonOrEpisode && isThisLang)) {
emptyResult
} else {
val potentialUrl = "${webtoonPath(url)}?title_no=$title_no"
fetchMangaDetails(SManga.create().apply { this.url = potentialUrl }).map {
it.url = potentialUrl
MangasPage(listOf(it), false)
}
}
} ?: emptyResult
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/$langCode/search?keyword=$query".toHttpUrl().newBuilder()
val uriPart = (filters.find { it is SearchType } as? SearchType)?.toUriPart() ?: ""
url.addQueryParameter("searchType", uriPart)
if (uriPart != "WEBTOON" && page > 1) url.addQueryParameter("page", page.toString())
return GET(url.build(), headers)
}
override fun searchMangaSelector() = "#content > div.card_wrap.search ul:not(#filterLayer) li a"
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = "div.more_area, div.paginate a[onclick] + a"
open fun parseDetailsThumbnail(document: Document): String? {
val picElement = document.select("#content > div.cont_box > div.detail_body")
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")").removeSurrounding("\"").removeSurrounding("'")
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
}
override fun mangaDetailsParse(document: Document): SManga {
val detailElement = document.select("#content > div.cont_box > div.detail_header > div.info")
val infoElement = document.select("#_asideDetail")
val manga = SManga.create()
manga.title = document.selectFirst("h1.subj, h3.subj")!!.text()
manga.author = detailElement.select(".author:nth-of-type(1)").first()?.ownText()
?: detailElement.select(".author_area").first()?.ownText()
manga.artist = detailElement.select(".author:nth-of-type(2)").first()?.ownText()
?: detailElement.select(".author_area").first()?.ownText() ?: manga.author
manga.genre = detailElement.select(".genre").joinToString(", ") { it.text() }
manga.description = infoElement.select("p.summary").text()
manga.status = infoElement.select("p.day_info").firstOrNull()?.text().orEmpty().toStatus()
manga.thumbnail_url = parseDetailsThumbnail(document)
return manga
}
open fun String.toStatus(): Int = when {
contains("UP") -> SManga.ONGOING
contains("COMPLETED") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun imageUrlParse(document: Document): String = document.select("img").first()!!.attr("src")
// Filters
override fun getFilterList(): FilterList {
return FilterList(
Header("Query can not be blank"),
Separator(),
SearchType(getOfficialList()),
)
}
override fun chapterListSelector() = "ul#_episodeList li[id*=episode]"
private class SearchType(vals: Array<Pair<String, String>>) : UriPartFilter("Official or Challenge", vals)
private fun getOfficialList() = arrayOf(
Pair("Any", ""),
Pair("Official only", "WEBTOON"),
Pair("Challenge only", "CHALLENGE"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a")
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = element.select("a > div.row > div.info > p.sub_title > span.ellipsis").text()
val select = element.select("a > div.row > div.num")
if (select.isNotEmpty()) {
chapter.name += " Ch. " + select.text().substringAfter("#")
}
if (element.select(".ico_bgm").isNotEmpty()) {
chapter.name += ""
}
chapter.date_upload = element.select("a > div.row > div.col > div.sub_info > span.date").text().let { chapterParseDate(it) } ?: 0
return chapter
}
open fun chapterParseDate(date: String): Long {
return try {
dateFormat.parse(date)?.time ?: 0
} catch (e: ParseException) {
0
}
}
override fun chapterListRequest(manga: SManga) = GET("https://m.webtoons.com" + manga.url, mobileHeaders)
override fun pageListParse(document: Document): List<Page> {
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
if (pages.isNotEmpty()) { return pages }
val docString = document.toString()
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body.string()).jsonObject
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
return motiontoonImages.entries
.filter { it.key.contains("layer") }
.mapIndexed { i, entry ->
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
}
}
companion object {
const val URL_SEARCH_PREFIX = "url:"
}
}

View File

@ -1,226 +0,0 @@
package eu.kanade.tachiyomi.multisrc.webtoons
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
open class WebtoonsTranslate(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val translateLangCode: String,
) : Webtoons(name, baseUrl, lang) {
// popularMangaRequest already returns manga sorted by latest update
override val supportsLatest = false
private val apiBaseUrl = "https://global.apis.naver.com".toHttpUrl()
private val mobileBaseUrl = "https://m.webtoons.com".toHttpUrl()
private val thumbnailBaseUrl = "https://mwebtoon-phinf.pstatic.net"
private val pageSize = 24
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.removeAll("Referer")
.add("Referer", mobileBaseUrl.toString())
private fun mangaRequest(page: Int, requeztSize: Int): Request {
val url = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedWebtoons_jsonp.json")!!
.newBuilder()
.addQueryParameter("orderType", "UPDATE")
.addQueryParameter("offset", "${(page - 1) * requeztSize}")
.addQueryParameter("size", "$requeztSize")
.addQueryParameter("languageCode", translateLangCode)
.build()
return GET(url, headers)
}
// Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC",
// and "TITLE_DESC". Pick UPDATE as the most useful sort.
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, pageSize)
override fun popularMangaParse(response: Response): MangasPage {
val offset = response.request.url.queryParameter("offset")!!.toInt()
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
throw Exception("Error getting popular manga: error code $responseCode")
}
val titles = result["result"]!!.jsonObject
val totalCount = titles["totalCount"]!!.jsonPrimitive.int
val mangaList = titles["titleList"]!!.jsonArray
.map { mangaFromJson(it.jsonObject) }
return MangasPage(mangaList, hasNextPage = totalCount > pageSize + offset)
}
private fun mangaFromJson(manga: JsonObject): SManga {
val relativeThumnailURL = manga["thumbnailIPadUrl"]?.jsonPrimitive?.contentOrNull
?: manga["thumbnailMobileUrl"]?.jsonPrimitive?.contentOrNull
return SManga.create().apply {
title = manga["representTitle"]!!.jsonPrimitive.content
author = manga["writeAuthorName"]!!.jsonPrimitive.content
artist = manga["pictureAuthorName"]?.jsonPrimitive?.contentOrNull ?: author
thumbnail_url = if (relativeThumnailURL != null) "$thumbnailBaseUrl$relativeThumnailURL" else null
status = SManga.UNKNOWN
url = mobileBaseUrl
.resolve("/translate/episodeList")!!
.newBuilder()
.addQueryParameter("titleNo", manga["titleNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("languageCode", translateLangCode)
.addQueryParameter("teamVersion", (manga["teamVersion"]?.jsonPrimitive?.intOrNull ?: 0).toString())
.build()
.toString()
}
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query)
}
}
/**
* Don't see a search function for Fan Translations, so let's do it client side.
* There's 75 webtoons as of 2019/11/21, a hardcoded request of 200 should be a sufficient request
* to get all titles, in 1 request, for quite a while
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = mangaRequest(page, 200)
private fun searchMangaParse(response: Response, query: String): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
throw Exception("Error getting manga: error code $responseCode")
}
val mangaList = result["result"]!!.jsonObject["titleList"]!!.jsonArray
.map { mangaFromJson(it.jsonObject) }
.filter { it.title.contains(query, ignoreCase = true) }
return MangasPage(mangaList, false)
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
override fun mangaDetailsParse(document: Document): SManga {
val getMetaProp = fun(property: String): String =
document.head().select("meta[property=\"$property\"]").attr("content")
var parsedAuthor = getMetaProp("com-linewebtoon:webtoon:author")
var parsedArtist = parsedAuthor
val authorSplit = parsedAuthor.split(" / ", limit = 2)
if (authorSplit.count() > 1) {
parsedAuthor = authorSplit[0]
parsedArtist = authorSplit[1]
}
return SManga.create().apply {
title = getMetaProp("og:title")
artist = parsedArtist
author = parsedAuthor
description = getMetaProp("og:description")
status = SManga.UNKNOWN
thumbnail_url = getMetaProp("og:image")
}
}
override fun chapterListSelector(): String = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
override fun chapterListRequest(manga: SManga): Request {
val mangaUrl = manga.url.toHttpUrl()
val titleNo = mangaUrl.queryParameter("titleNo")
val teamVersion = mangaUrl.queryParameter("teamVersion")
val chapterListUrl = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedEpisodes_jsonp.json")!!
.newBuilder()
.addQueryParameter("titleNo", titleNo)
.addQueryParameter("languageCode", translateLangCode)
.addQueryParameter("offset", "0")
.addQueryParameter("limit", "10000")
.addQueryParameter("teamVersion", teamVersion)
.toString()
return GET(chapterListUrl, mobileHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
val message = result["message"]?.jsonPrimitive?.content ?: "error code $responseCode"
throw Exception("Error getting chapter list: $message")
}
return result["result"]!!.jsonObject["episodes"]!!.jsonArray
.filter { it.jsonObject["translateCompleted"]!!.jsonPrimitive.boolean }
.map { parseChapterJson(it.jsonObject) }
.reversed()
}
private fun parseChapterJson(obj: JsonObject): SChapter = SChapter.create().apply {
name = obj["title"]!!.jsonPrimitive.content + " #" + obj["episodeSeq"]!!.jsonPrimitive.int
chapter_number = obj["episodeSeq"]!!.jsonPrimitive.int.toFloat()
date_upload = obj["updateYmdt"]!!.jsonPrimitive.long
scanlator = obj["teamVersion"]!!.jsonPrimitive.int.takeIf { it != 0 }?.toString() ?: "(wiki)"
val chapterUrl = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json")!!
.newBuilder()
.addQueryParameter("titleNo", obj["titleNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("episodeNo", obj["episodeNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("languageCode", obj["languageCode"]!!.jsonPrimitive.content)
.addQueryParameter("teamVersion", obj["teamVersion"]!!.jsonPrimitive.int.toString())
.toString()
setUrlWithoutDomain(chapterUrl)
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(apiBaseUrl.resolve(chapter.url)!!, headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = json.parseToJsonElement(response.body.string()).jsonObject
return result["result"]!!.jsonObject["imageInfo"]!!.jsonArray
.mapIndexed { i, jsonEl ->
Page(i, "", jsonEl.jsonObject["imageUrl"]!!.jsonPrimitive.content)
}
}
override fun getFilterList(): FilterList = FilterList()
}

View File

@ -129,19 +129,22 @@ abstract class YuYu(
genre = details.select(".genre-tag").joinToString { it.text() }
description = details.selectFirst(".sinopse p")?.text()
details.selectFirst(".manga-meta > div")?.ownText()?.let {
status = when (it.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
status = it.toStatus()
}
setUrlWithoutDomain(document.location())
}
private fun SManga.fetchMangaId(): String {
val document = client.newCall(mangaDetailsRequest(this)).execute().asJsoup()
protected fun String.toStatus(): Int {
return when (lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
protected open fun getMangaId(manga: SManga): String {
val document = client.newCall(mangaDetailsRequest(manga)).execute().asJsoup()
return document.select("script")
.map(Element::data)
.firstOrNull(MANGA_ID_REGEX::containsMatchIn)
@ -159,11 +162,11 @@ abstract class YuYu(
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val mangaId = getMangaId(manga)
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto<String>>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
@ -194,7 +197,7 @@ abstract class YuYu(
// ============================== Utilities ===========================
@Serializable
class ChaptersDto(val chapters: String, private val remaining: Int) {
class ChaptersDto<T>(val chapters: T, private val remaining: Int) {
fun hasNext() = remaining > 0
}

View File

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

View File

@ -0,0 +1,83 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
abstract class ZeroTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
open val imageLocation: String = "images"
private val sourceLocation: String get() = "$cdnUrl/$imageLocation"
// =========================== Popular ================================
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList())
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =========================== Latest ===================================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesParse(response: Response): MangasPage =
MangasPage(response.toDto<LatestDto>().toSMangaList(sourceLocation), hasNextPage = false)
// =========================== Search =================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
val mangas = dto.mangas.map { it.toSManga(sourceLocation) }
return MangasPage(mangas, hasNextPage = dto.hasNextPage())
}
// =========================== Details =================================
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
// =========================== Chapter =================================
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
// =========================== Pages ===================================
override fun pageListParse(response: Response): List<Page> =
response.toDto<PageDto>().toPageList(sourceLocation)
override fun imageUrlParse(response: Response) = ""
// =========================== Utilities ===============================
inline fun <reified T> Response.toDto(): T {
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
return jsonString.parseAs<T>()
}
}

View File

@ -0,0 +1,139 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class Props<T>(
@JsonNames("comic_infos", "chapter", "new_chapters")
val content: T,
)
@Serializable
class LatestDto(
private val props: Props<List<Comic>>,
) {
fun toSMangaList(srcPath: String) = props.content.map { it.comic.toSManga(srcPath) }
@Serializable
class Comic(
val comic: MangaDto,
)
}
@Serializable
class MangaDetailsDto(
private val props: Props<MangaDto>,
) {
fun toSManga(srcPath: String) = props.content.toSManga(srcPath)
fun toSChapterList() = props.content.chapters!!.map { it.toSChapter() }
}
@Serializable
class PageDto(
val props: Props<ChapterWrapper>,
) {
fun toPageList(srcPath: String): List<Page> {
return props.content.chapter.pages
.filter { it.pathSegment.contains("xml").not() }
.mapIndexed { index, path ->
Page(index, imageUrl = "$srcPath/${path.pathSegment}")
}
}
@Serializable
class ChapterWrapper(
val chapter: Chapter,
)
@Serializable
class Chapter(
val pages: List<Image>,
)
@Serializable
class Image(
@SerialName("page_path")
val pathSegment: String,
)
}
@Serializable
class SearchDto(
@SerialName("comics")
private val page: PageDto,
) {
val mangas: List<MangaDto> get() = page.data
fun hasNextPage() = page.currentPage < page.lastPage
@Serializable
class PageDto(
val `data`: List<MangaDto>,
@SerialName("last_page")
val lastPage: Int = 0,
@SerialName("current_page")
val currentPage: Int = 0,
)
}
@Serializable
class MangaDto(
val title: String,
val description: String?,
@SerialName("cover")
val thumbnailUrl: String?,
val slug: String,
val status: List<ValueDto>? = emptyList(),
val genres: List<ValueDto>? = emptyList(),
val chapters: List<ChapterDto>? = emptyList(),
) {
fun toSManga(srcPath: String) = SManga.create().apply {
title = this@MangaDto.title
description = this@MangaDto.description?.let { Jsoup.parseBodyFragment(it).text() }
this.thumbnail_url = thumbnailUrl?.let { "$srcPath/$it" }
status = when (this@MangaDto.status?.firstOrNull()?.name?.lowercase()) {
"em andamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
genre = genres?.joinToString { it.name }
url = "/comic/$slug"
}
@Serializable
class ValueDto(
val name: String,
)
}
@Serializable
class ChapterDto(
@SerialName("chapter_number")
val number: Float,
@SerialName("chapter_path")
val path: String,
@SerialName("created_at")
val createdAt: String,
) {
fun toSChapter() = SChapter.create().apply {
name = number.toString()
chapter_number = number
date_upload = dateFormat.tryParse(createdAt)
url = "/chapter/$path"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -8,7 +8,6 @@ loadAllIndividualExtensions()
* ===================================== COMMON CONFIGURATION ======================================
*/
include(":core")
include(":utils")
// Load all modules under /lib
File(rootDir, "lib").eachDir { include("lib:${it.name}") }

View File

@ -1,9 +1,8 @@
ext {
extName = 'Comic Growl'
extClass = '.ComicGrowl'
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
extVersionCode = 7
isNsfw = false
}

View File

@ -1,63 +1,212 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
// TODO: get manga status
// TODO: filter by status
// TODO: change cdnUrl as a array(upstream)
class ComicGrowl : GigaViewer(
"コミックグロウル",
"https://comic-growl.com",
"all",
"https://cdn-img.comic-growl.com/public/page",
) {
class ComicGrowl(
override val lang: String = "all",
override val baseUrl: String = "https://comic-growl.com",
override val name: String = "コミックグロウル",
override val supportsLatest: Boolean = true,
) : ParsedHttpSource() {
override val publisher = "BUSHIROAD WORKS"
override val client = super.client.newBuilder()
.addNetworkInterceptor(ImageDescrambler::interceptor)
.build()
override val chapterListMode = CHAPTER_LIST_LOCKED
override val supportsLatest: Boolean = true
override val client: OkHttpClient =
super.client.newBuilder().addInterceptor(::imageIntercept).build()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// Show only ongoing works
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.select("h5").text()
thumbnail_url = element.select("div > img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
override fun headersBuilder(): Headers.Builder {
return super.headersBuilder().set("Referer", "$baseUrl/")
}
override fun latestUpdatesSelector() =
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/ranking/manga", headers)
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
title = element.select("div.data h3").text()
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
override fun popularMangaNextPageSelector() = null
override fun popularMangaSelector() = ".ranking-item"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst(".title-text")!!.text()
setImageUrlFromElement(element)
}
}
override fun getCollections(): List<Collection> = listOf(
Collection("連載作品", ""),
)
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst(".series-h-info")!!
val authorElements = infoElement.select(".series-h-credit-user-item .article-text")
val updateDateElement = infoElement.selectFirst(".series-h-tag-label")
return SManga.create().apply {
title = infoElement.selectFirst("h1 > span:not(.g-hidden)")!!.text()
author = authorElements.joinToString { it.text() }
description = infoElement.selectFirst(".series-h-credit-info-text-text p")?.wholeText()?.trim()
setImageUrlFromElement(document.selectFirst(".series-h-img"))
status = if (updateDateElement != null) SManga.ONGOING else SManga.COMPLETED
}
}
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "/list", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).mapIndexed { index, element ->
chapterFromElement(element).apply {
chapter_number = index.toFloat()
if (url.isEmpty()) { // need login, set a dummy url and append lock icon for chapter name
val hasLockElement = element.selectFirst(".g-payment-article.wait-free-enabled")
url = response.request.url.newBuilder().fragment("$index-$DUMMY_URL_SUFFIX").build().toString()
name = (if (hasLockElement != null) LOCK_ICON else PAY_ICON) + name
}
}
}
}
override fun chapterListSelector() = ".article-ep-list-item-img-link"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.absUrl("data-href"))
name = element.selectFirst(".series-ep-list-item-h-text")!!.text()
setUploadDate(element.selectFirst(".series-ep-list-date-time"))
scanlator = PUBLISHER
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.endsWith(DUMMY_URL_SUFFIX)) {
throw Exception("Login required to see this chapter")
}
return super.pageListRequest(chapter)
}
override fun pageListParse(document: Document): List<Page> {
val pageList = mutableListOf<Page>()
// Get some essential info from document
val viewer = document.selectFirst("#comici-viewer")!!
val comiciViewerId = viewer.attr("comici-viewer-id")
val memberJwt = viewer.attr("data-member-jwt")
val requestUrl = "$baseUrl/book/contentsInfo".toHttpUrl().newBuilder()
.addQueryParameter("comici-viewer-id", comiciViewerId)
.addQueryParameter("user-id", memberJwt)
.addQueryParameter("page-from", "0")
// Initial request to get total pages
val initialRequest = GET(requestUrl.addQueryParameter("page-to", "1").build(), headers)
client.newCall(initialRequest).execute().use { initialResponseRaw ->
if (!initialResponseRaw.isSuccessful) {
throw Exception("Failed to get page list")
}
// Get all pages
val pageTo = initialResponseRaw.parseAs<PageResponse>().totalPages.toString()
val getAllPagesUrl = requestUrl.setQueryParameter("page-to", pageTo).build()
val getAllPagesRequest = GET(getAllPagesUrl, headers)
client.newCall(getAllPagesRequest).execute().use {
if (!it.isSuccessful) {
throw Exception("Failed to get page list")
}
it.parseAs<PageResponse>().result.forEach { resultItem ->
// Origin scramble string is something like [6, 9, 14, 15, 8, 3, 4, 12, 1, 5, 0, 7, 13, 2, 11, 10]
val scramble = resultItem.scramble.drop(1).dropLast(1).replace(", ", "-")
// Add fragment to let interceptor descramble the image
val imageUrl = resultItem.imageUrl.toHttpUrl().newBuilder().fragment(scramble).build()
pageList.add(
Page(index = resultItem.sort, imageUrl = imageUrl.toString()),
)
}
}
}
return pageList
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
val searchUrl = "$baseUrl/search".toHttpUrl().newBuilder()
.setQueryParameter("keyword", query)
.setQueryParameter("page", page.toString())
.build()
return GET(searchUrl, headers)
}
return GET(url.build(), headers)
override fun searchMangaNextPageSelector() = null
override fun searchMangaSelector() = ".series-list a"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.absUrl("href"))
title = element.selectFirst(".manga-title")!!.text()
setImageUrlFromElement(element)
}
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesNextPageSelector() = null
override fun latestUpdatesSelector() = "h2:contains(新連載) + .feature-list > .feature-item"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst("h3")!!.text()
setImageUrlFromElement(element)
}
// ========================================= Helper Functions =====================================
companion object {
private const val PUBLISHER = "BUSHIROAD WORKS"
private val imageUrlRegex by lazy { Regex("^.*?webp") }
private val DATE_PARSER by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT) }
private const val DUMMY_URL_SUFFIX = "NeedLogin"
private const val PAY_ICON = "💴 "
private const val LOCK_ICON = "🔒 "
}
/**
* Set cover image url from [element] for [SManga]
*/
private fun SManga.setImageUrlFromElement(element: Element?) {
if (element == null) {
return
}
return GET(baseUrl, headers) // Currently just get all ongoing works
val match = imageUrlRegex.find(element.selectFirst("source")!!.attr("data-srcset"))
// Add missing protocol
if (match != null) {
this.thumbnail_url = "https:${match.value}"
}
}
/**
* Set date_upload to [SChapter], parsing from string like "3月31日" to UNIX Epoch time.
*/
private fun SChapter.setUploadDate(element: Element?) {
if (element == null) {
return
}
this.date_upload = DATE_PARSER.tryParse(element.attr("datetime"))
}
}

View File

@ -0,0 +1,70 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
object ImageDescrambler {
// Left-top corner position
private class TilePos(val x: Int, val y: Int)
/**
* Interceptor to descramble the image.
*/
fun interceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val scramble = request.url.fragment ?: return response // return if no scramble fragment
val tiles = buildList {
scramble.split("-").forEachIndexed { index, s ->
val scrambleInt = s.toInt()
add(index, TilePos(scrambleInt / 4, scrambleInt % 4))
}
}
val scrambledImg = BitmapFactory.decodeStream(response.body.byteStream())
val descrambledImg = drawDescrambledImage(scrambledImg, scrambledImg.width, scrambledImg.height, tiles)
val output = ByteArrayOutputStream()
descrambledImg.compress(Bitmap.CompressFormat.JPEG, 90, output)
val body = output.toByteArray().toResponseBody("image/jpeg".toMediaType())
return response.newBuilder().body(body).build()
}
private fun drawDescrambledImage(rawImage: Bitmap, width: Int, height: Int, tiles: List<TilePos>): Bitmap {
// Prepare canvas
val descrambledImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(descrambledImg)
// Tile width and height(4x4)
val tileWidth = width / 4
val tileHeight = height / 4
// Draw rect
var count = 0
for (x in 0..3) {
for (y in 0..3) {
val desRect = Rect(x * tileWidth, y * tileHeight, (x + 1) * tileWidth, (y + 1) * tileHeight)
val srcRect = Rect(
tiles[count].x * tileWidth,
tiles[count].y * tileHeight,
(tiles[count].x + 1) * tileWidth,
(tiles[count].y + 1) * tileHeight,
)
canvas.drawBitmap(rawImage, srcRect, desRect, null)
count++
}
}
return descrambledImg
}
}

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import kotlinx.serialization.Serializable
@Serializable
class PageResponse(
val totalPages: Int,
val result: List<PageResponseResult>,
)
@Serializable
class PageResponseResult(
val imageUrl: String,
val scramble: String,
val sort: Int,
)

View File

@ -22,3 +22,6 @@ score_position_top=Top
score_position_middle=Middle
score_position_bottom=Bottom
score_position_none=Hide Score
chapter_score_filtering_title=Automatically de-duplicate chapters
chapter_score_filtering_on=For each chapter, only displays the scanlator with the highest score
chapter_score_filtering_off=Does not filterout any chapters based on score (any other scanlator filtering will still apply)

View File

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

View File

@ -178,6 +178,20 @@ abstract class Comick(
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_SCORE_FILTERING_PREF
title = intl["chapter_score_filtering_title"]
summaryOff = intl["chapter_score_filtering_off"]
summaryOn = intl["chapter_score_filtering_on"]
setDefaultValue(CHAPTER_SCORE_FILTERING_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(CHAPTER_SCORE_FILTERING_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.ignoredGroups: Set<String>
@ -224,6 +238,9 @@ abstract class Comick(
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
private val SharedPreferences.chapterScoreFiltering: Boolean
get() = getBoolean(CHAPTER_SCORE_FILTERING_PREF, CHAPTER_SCORE_FILTERING_DEFAULT)
override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/")
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
@ -546,9 +563,19 @@ abstract class Comick(
publishedChapter && noGroupBlock
}
.filterOnScore(preferences.chapterScoreFiltering)
.map { it.toSChapter(mangaUrl) }
}
private fun List<Chapter>.filterOnScore(shouldFilter: Boolean): Collection<Chapter> {
if (shouldFilter) {
return groupBy { it.chap }
.map { (_, chapters) -> chapters.maxBy { it.score } }
} else {
return this
}
}
private val publishedDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
@ -626,6 +653,8 @@ abstract class Comick(
const val SCORE_POSITION_DEFAULT = "top"
private const val LOCAL_TITLE_PREF = "LocalTitle"
private const val LOCAL_TITLE_DEFAULT = false
private const val CHAPTER_SCORE_FILTERING_PREF = "ScoreAutoFiltering"
private const val CHAPTER_SCORE_FILTERING_DEFAULT = false
private const val LIMIT = 20
private const val CHAPTERS_LIMIT = 99999
}

View File

@ -199,10 +199,14 @@ class Chapter(
private val title: String = "",
@SerialName("created_at") private val createdAt: String = "",
@SerialName("publish_at") val publishedAt: String = "",
private val chap: String = "",
val chap: String = "",
private val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(),
@SerialName("up_count") private val upCount: Int,
@SerialName("down_count") private val downCount: Int,
) {
val score get() = upCount - downCount
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$hid-chapter-$chap-$lang"
name = beautifyChapterName(vol, chap, title)

View File

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

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
@ -20,7 +21,6 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -43,14 +43,6 @@ class DeviantArt : HttpSource(), ConfigurableSource {
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
}
private fun parseDate(dateStr: String?): Long {
return try {
dateFormat.parse(dateStr ?: "")!!.time
} catch (_: ParseException) {
0L
}
}
override fun popularMangaRequest(page: Int): Request {
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
}
@ -94,9 +86,9 @@ class DeviantArt : HttpSource(), ConfigurableSource {
return SManga.create().apply {
setUrlWithoutDomain(response.request.url.toString())
author = document.title().substringBefore(" ")
title = when (artistInTitle) {
true -> "$author - $galleryName"
false -> galleryName
title = when {
artistInTitle -> "$author - $galleryName"
else -> galleryName
}
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
@ -142,7 +134,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text())
name = it.selectFirst("title")!!.text()
date_upload = parseDate(it.selectFirst("pubDate")?.text())
date_upload = dateFormat.tryParse(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text()
}
}
@ -162,16 +154,17 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) {
null -> listOf(Page(0, imageUrl = firstImageUrl))
else -> buttons.mapIndexed { i, button ->
val buttons = document.selectFirst("[draggable=false]")?.children()
return if (buttons == null) {
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
listOf(Page(0, imageUrl = imageUrl))
} else {
buttons.mapIndexed { i, button ->
// Remove everything past "/v1/" to get original instead of thumbnail
val imageUrl = button.selectFirst("img")?.absUrl("src")?.substringBefore("/v1/")
// But need to preserve the query parameter where the token is
val imageUrl = button.selectFirst("img")?.absUrl("src")
?.replaceFirst(Regex("""/v1(/.*)?(?=\?)"""), "")
Page(i, imageUrl = imageUrl)
}.also {
// First image needs token to get original, which is included in firstImageUrl
it[0].imageUrl = firstImageUrl
}
}
}

View File

@ -1,168 +0,0 @@
package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
open class EternalMangas(
lang: String,
private val internalLang: String,
) : MangaEsp(
"EternalMangas",
"https://eternalmangas.com",
lang,
) {
override val useApiSearch = true
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("views", false))
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("updated_at", false))
}
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
return this.filter { it.language == internalLang }.toMutableList()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/comics", headers)
}
private val dataUrl = "https://raw.githubusercontent.com/bapeey/extensions-tools/refs/heads/main/keiyoushi/eternalmangas/values.txt"
override fun searchMangaParse(
response: Response,
page: Int,
query: String,
filters: FilterList,
): MangasPage {
val (apiComicsUrl, jsonHeaders, useApi, scriptSelector, comicsRegex) = client.newCall(GET(dataUrl)).execute().body.string().split("\n")
val apiSearch = useApi == "1"
comicsList = if (apiSearch) {
val headersJson = json.parseToJsonElement(jsonHeaders).jsonObject
val apiHeaders = headersBuilder()
headersJson.forEach { (key, jsonElement) ->
var value = jsonElement.jsonPrimitive.contentOrNull.orEmpty()
if (value.startsWith("1-")) {
val match = value.substringAfter("-").toRegex().find(response.body.string())
value = match?.groupValues?.get(1).orEmpty()
} else {
value = value.substringAfter("-")
}
apiHeaders.add(key, value)
}
val apiResponse = client.newCall(GET(apiComicsUrl, apiHeaders.build())).execute()
json.decodeFromString<List<SeriesDto>>(apiResponse.body.string()).toMutableList()
} else {
val script = response.asJsoup().select(scriptSelector).joinToString { it.data() }
val jsonString = comicsRegex.toRegex().find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
}
return parseComicsList(page, query, filters)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = jsRedirect(response)
MANGA_DETAILS_REGEX.find(body)?.groupValues?.get(1)?.let {
val unescapedJson = it.unescape()
return json.decodeFromString<SeriesDto>(unescapedJson).toSMangaDetails()
}
val document = Jsoup.parse(body)
with(document.selectFirst("div#info")!!) {
title = select("div:has(p.font-bold:contains(Títuto)) > p.text-sm").text()
author = select("div:has(p.font-bold:contains(Autor)) > p.text-sm").text()
artist = select("div:has(p.font-bold:contains(Artista)) > p.text-sm").text()
genre = select("div:has(p.font-bold:contains(Género)) > p.text-sm > span").joinToString { it.ownText() }
}
description = document.select("div#sinopsis p").text()
thumbnail_url = document.selectFirst("div.contenedor img.object-cover")?.imgAttr()
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
override fun chapterListParse(response: Response): List<SChapter> {
val body = jsRedirect(response)
MANGA_DETAILS_REGEX.find(body)?.groupValues?.get(1)?.let {
val unescapedJson = it.unescape()
val series = json.decodeFromString<SeriesDto>(unescapedJson)
return series.chapters.map { chapter -> chapter.toSChapter(seriesPath, series.slug) }
}
val document = Jsoup.parse(body)
return document.select("div.contenedor > div.grid > div > a").map {
SChapter.create().apply {
name = it.selectFirst("span.text-sm")!!.text()
date_upload = try {
it.selectFirst("span.chapter-date")?.attr("data-date")?.let { date ->
dateFormat.parse(date)?.time
} ?: 0
} catch (e: ParseException) {
0
}
setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
}
}
}
override fun pageListParse(response: Response): List<Page> {
val doc = Jsoup.parse(jsRedirect(response))
return doc.select("main > img").mapIndexed { i, img ->
Page(i, imageUrl = img.imgAttr())
}
}
private fun jsRedirect(response: Response): String {
var body = response.body.string()
val document = Jsoup.parse(body)
document.selectFirst("body > form[method=post], body > div[hidden] > form[method=post]")?.let {
val action = it.attr("action")
val inputs = it.select("input")
val form = FormBody.Builder()
inputs.forEach { input ->
form.add(input.attr("name"), input.attr("value"))
}
body = client.newCall(POST(action, headers, form.build())).execute().body.string()
}
return body
}
private fun createSortFilter(value: String, ascending: Boolean = false): FilterList {
val sortProperties = getSortProperties()
val index = sortProperties.indexOfFirst { it.value == value }.takeIf { it >= 0 } ?: 0
return FilterList(
SortByFilter("", sortProperties).apply {
state = Filter.Sort.Selection(index, ascending)
},
)
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.source.SourceFactory
class EternalMangasFactory : SourceFactory {
override fun createSources() = listOf(
EternalMangasES(),
EternalMangasEN(),
EternalMangasPTBR(),
)
}
class EternalMangasES : EternalMangas("es", "es")
class EternalMangasEN : EternalMangas("en", "en")
class EternalMangasPTBR : EternalMangas("pt-BR", "pt")

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hentai Cosplay'
extClass = '.HentaiCosplay'
extVersionCode = 4
extVersionCode = 5
isNsfw = true
}

View File

@ -198,7 +198,7 @@ class HentaiCosplay : HttpSource() {
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
SChapter.create().apply {
name = "Gallery"
url = manga.url.removeSuffix("/").plus("/attachment/1/")
url = manga.url.replace("/image/", "/story/")
date_upload = runCatching {
dateFormat.parse(dateCache[manga.url]!!)!!.time
}.getOrDefault(0L)
@ -209,21 +209,13 @@ class HentaiCosplay : HttpSource() {
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val pageUrl = document.location().substringBeforeLast("/1/")
val totalPages = document.selectFirst("#right_sidebar > h3, #title > h2")
?.text()?.trim()
?.run { pagesRegex.find(this)?.groupValues?.get(1) }
?.toIntOrNull()
?: return emptyList()
val pages = (1..totalPages).map {
Page(it, "$pageUrl/$it/")
}
pages[0].imageUrl = imageUrlParse(document)
return pages
return document.select("amp-img[src*=upload]")
.mapIndexed { index, element ->
Page(
index = index,
imageUrl = element.attr("src"),
)
}
}
override fun imageUrlParse(response: Response) = imageUrlParse(response.asJsoup())

View File

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

View File

@ -15,6 +15,7 @@ import keiyoushi.utils.tryParse
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -24,12 +25,14 @@ import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import rx.Observable
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.LinkedHashSet
import java.util.LinkedList
import java.util.Locale
import kotlin.math.min
@ -53,9 +56,6 @@ class Hitomi(
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageUrlInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
.build()
override fun headersBuilder() = super.headersBuilder()
@ -118,7 +118,22 @@ class Hitomi(
}
}
return client.newCall(request).awaitSuccess().use { it.body.bytes() }
val tries = 5
repeat(tries) { attempt ->
try {
return client.newCall(request).awaitSuccess().use { it.body.bytes() }
} catch (e: StreamResetException) {
if (e.errorCode == ErrorCode.INTERNAL_ERROR) {
if (attempt == tries - 1) throw e // last attempt, rethrow
Log.e(name, "Stream reset attempt ${attempt + 1}", e)
delay((attempt + 1).seconds)
} else {
throw e
}
}
}
throw Exception("Unreachable code")
}
private suspend fun hitomiSearch(
@ -294,8 +309,6 @@ class Hitomi(
val inbuf = getRangedResponse(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>()
val buffer =
ByteBuffer
.wrap(inbuf)
@ -312,6 +325,9 @@ class Hitomi(
"inbuf.byteLength ${inbuf.size} != expected_length $expectedLength"
}
// we know total number so avoid internal resize overhead
val galleryIDs = LinkedHashSet<Int>(numberOfGalleryIDs, 1.0f)
for (i in 0.until(numberOfGalleryIDs))
galleryIDs.add(buffer.int)
@ -390,12 +406,16 @@ class Hitomi(
}
val bytes = getRangedResponse(nozomiAddress, range)
val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
val size = arrayBuffer.remaining() / Int.SIZE_BYTES
// we know total number so avoid internal resize overhead
val nozomi = LinkedHashSet<Int>(size, 1.0f)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
@ -667,20 +687,6 @@ class Hitomi(
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
}
private fun streamResetRetry(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: StreamResetException) {
Log.e(name, "reset", e)
if (e.message.orEmpty().contains("INTERNAL_ERROR")) {
Thread.sleep(2.seconds.inWholeMilliseconds)
chain.proceed(chain.request())
} else {
throw e
}
}
}
private fun imageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host != IMAGE_LOOPBACK_HOST) {

View File

@ -24,7 +24,7 @@ class ImageFile(
val hash: String,
private val name: String,
) {
val isGif get() = name.endsWith(".gif")
val isGif get() = name.endsWith(".gif") || name.endsWith(".webp")
}
@Serializable

View File

@ -1,7 +1,7 @@
ext {
extName = 'HOLONOMETRIA'
extClass = '.HolonometriaFactory'
extVersionCode = 2
extVersionCode = 3
isNsfw = false
}

View File

@ -8,12 +8,12 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.tryParse
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class Holonometria(
override val lang: String,
@ -22,31 +22,23 @@ class Holonometria(
override val name = "HOLONOMETRIA"
override val baseUrl = "https://alt.hololive.tv"
override val baseUrl = "https://holoearth.com"
override val supportsLatest = false
override val client = network.cloudflareClient.newBuilder()
.readTimeout(60, TimeUnit.SECONDS)
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/${langPath}alt/holonometria/manga/", headers)
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/holonometria/$langPath", headers)
override fun popularMangaSelector() = "#Story article:has(a[href*=/manga/])"
override fun popularMangaSelector() = ".manga__item"
override fun popularMangaNextPageSelector() = null
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = element.select(".ttl").text()
title = element.select(".manga__title").text()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
GET("$baseUrl/holonometria/$langPath#${query.trim()}", headers)
GET("$baseUrl/${langPath}alt/holonometria/manga/#${query.trim()}", headers)
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
@ -64,10 +56,10 @@ class Holonometria(
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".md-ttl__pages").text()
thumbnail_url = document.select(".mangainfo img").attr("abs:src")
description = document.select(".mangainfo aside").text()
val info = document.select(".mangainfo footer").html().split("<br>")
title = document.select(".alt-nav__met-sub-link.is-current").text()
thumbnail_url = document.select(".manga-detail__thumb img").attr("abs:src")
description = document.select(".manga-detail__caption").text()
val info = document.select(".manga-detail__person").html().split("<br>")
author = info.firstOrNull { desc -> manga.any { desc.contains(it, true) } }
?.substringAfter("")
?.substringAfter(":")
@ -80,57 +72,23 @@ class Holonometria(
?.replace("&amp;", "&")
}
override fun chapterListRequest(manga: SManga) =
paginatedChapterListRequest(manga.url, 1)
override fun chapterListRequest(manga: SManga) = GET("$baseUrl/${manga.url}", headers)
private fun paginatedChapterListRequest(mangaUrl: String, page: Int) =
GET("$baseUrl$mangaUrl".removeSuffix("/") + if (page == 1) "/" else "/page/$page/", headers)
override fun chapterListSelector() = ".manga-detail__list .manga-detail__list-item"
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val mangaUrl = response.request.url.toString()
.substringAfter(baseUrl)
.substringBefore("page/")
val chapters = document.select(chapterListSelector())
.map(::chapterFromElement)
.toMutableList()
val lastPage = document.select(".pagenation-list a").last()
?.text()?.toIntOrNull() ?: return chapters
for (page in 2..lastPage) {
val request = paginatedChapterListRequest(mangaUrl, page)
val newDocument = client.newCall(request).execute().asJsoup()
val moreChapters = newDocument.select(chapterListSelector())
.map(::chapterFromElement)
chapters.addAll(moreChapters)
}
return chapters
}
override fun chapterListSelector() = "#Archive article"
override fun chapterListParse(response: Response): List<SChapter> =
super.chapterListParse(response).reversed()
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
name = element.select(".ttl").text()
date_upload = element.selectFirst(".data--date")?.text().parseDate()
scanlator = element.selectFirst(".data--category")?.text()
}
private fun String?.parseDate(): Long {
return runCatching {
dateFormat.parse(this!!)!!.time
}.getOrDefault(0L)
name = element.select(".manga-detail__list-title").text()
date_upload = dateFormat.tryParse(element.selectFirst(".manga-detail__list-date")?.text())
}
override fun pageListParse(document: Document): List<Page> {
return document.select("#js-mangaviewer img").mapIndexed { idx, img ->
return document.select(".manga-detail__swiper-wrapper img").mapIndexed { idx, img ->
Page(idx, "", img.attr("abs:src"))
}
}.reversed()
}
companion object {
@ -138,7 +96,7 @@ class Holonometria(
private val script = listOf("script", "naskah", "脚本")
private val dateFormat by lazy {
SimpleDateFormat("yy.MM.dd", Locale.ENGLISH)
SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH)
}
}

View File

@ -1,10 +0,0 @@
ext {
extName = 'KDT Scans'
extClass = '.KdtScans'
themePkg = 'madara'
baseUrl = 'https://kdtscans.com'
overrideVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

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