Compare commits

..

273 Commits

Author SHA1 Message Date
mrtear
cbfdd982ff
Add KaynScans (#10792)
All checks were successful
CI / Prepare job (push) Successful in 7s
CI / Build individual modules (push) Successful in 8m20s
CI / Publish repo (push) Successful in 55s
kaynscans
2025-10-03 00:00:23 +01:00
manti
9a738a668f
MagComi: fix search and collection (#10790)
fix search and collection
2025-10-03 00:00:23 +01:00
Smol Ame
5fdd51e172
Luscious: Update "API" preference mirror URL (#10781)
* Luscious: Bump versionCode

* Luscious: Update "API" mirror URL
2025-10-03 00:00:23 +01:00
manti
b6a4f40609
Add Ichicomi (#10778)
* add ichicomi

* better latest, add genres

* remove latest, add image interceptor

* fix kuragebunch

* generalize to gigaviewer

* bump

* small fix

* merge

* fix and separate

* cleanup
2025-10-03 00:00:23 +01:00
manti
744c401773
GigaViewer: fix scrambled image detection (#10782)
fix
2025-10-03 00:00:23 +01:00
Chopper
d0e9279214
SakuraMangas: Fix (#10777)
* Fix Cloudflare and encrypted pages

* Bump version
2025-10-03 00:00:23 +01:00
nicki
08b627c8e7
Move MangaPlus Creators to new domain (#10337)
* start switching to new URLs for popular/manga/etc

* fix popular

* fix latest

* minor change to manga parse

* refactor popular, fix search

* fix search/popular selector

* fix chapters/pages

* fixes from debugging on android emulator

* increment ext version

* support paginated chapter lists

* break doesn't break

well, that's not true, it did work once the extension was freshly
installed. but I liked the alternative so I thought why not. can
be removed if needed

* cleanup

* add TODOs

* add intents to urls and search prefixes

support both old and new domains (since it all redirects, bless them)

* move around toSManga

pro: we get setUrlWithoutDomain
con: we lose this@<data-class-name>.title

* add filter screen

* debug search

* fix pathPattern

`..*` is the same as `.+`. however, the latter requires adding
`advancedPathPattern` instead

* what the intent: fix classdefnotfoundexception

* categorise into sections

* prefer helper functions from `utils`

* Change inline import to explicit

* inline baseUrl

* inline apiUrl

* remove superfluous header modifications

* always pass headers on new requests

* no need to convert HttpUrl to String

* make helper functions private

* use selectFirst instead of select, assert non-null

* make sub classes defined under filters private

* lint

* prefer not data but class

* Revert "break doesn't break"

This reverts commit 23b2cfe46c0f57214443e138a06cadbef0cccb61.

* lint

* better chapterNumber fail case ( -1f instead of 1f )

* lint
2025-10-03 00:00:23 +01:00
Mochammad Nopal Attasya
0ec1a28ed7
Update Tojimangas URL (#10757)
* Update Tojimangas URL

* Update build.gradle
2025-10-03 00:00:23 +01:00
manti
1ec8554fe2
EZmanga: from HeanCms to Iken (#10755)
theme switch
2025-10-03 00:00:23 +01:00
WorldTeacher
9c695d0e65
[Komga] Add login using API key (#10752)
bump version, add api key login

Co-authored-by: WorldTeacher <coding_contact@pm.me>
2025-10-03 00:00:22 +01:00
toomyzoom
93ba18cd9c
HentaiNexus: Add SChapter::date_upload (#10743)
* HentaiNexus: Add SChapter::date_upload

 Add SChapter::date_upload

* Update build.gradle

* Update HentaiNexus.kt to address comments

* Update HentaiNexus.kt #2

Update to deal with Published row not available
2025-10-03 00:00:22 +01:00
Aurel
ac069dd2ec
[poseidonscans] - Fixes broken search due to site changes. (#10740)
* Fix search and add pagination

* Fix URL decoding in PoseidonScans extension
2025-10-03 00:00:22 +01:00
bapeey
8c74ea91cd
Atsumaru: Fetch all chapter list pages (#10749)
add chapter list pagination
2025-10-03 00:00:22 +01:00
Fermín Cirella
fe47d20f67
Add Yabai (#10734)
* Add Yabai

* Reorder code

* Use utils
2025-10-03 00:00:22 +01:00
marioplus
41e64ac576
fix(bakamh): fix unable to load chapter (#10713)
* fix(bakamh): fix unable to load chapter

* fix(bakamh): fix unable to load chapter

* fix(bakamh): improve element selection stability

- Avoid directly using li class as selector
2025-10-03 00:00:22 +01:00
manti
7d62c04507
Fix Ganma: use new API GraphQL (#10687)
* api change to graphql

* fix web entries

* add afterword page, reduce requests

* some review changes

* serializable class templates
2025-10-03 00:00:22 +01:00
marioplus
33f4d5f8c0
fix(YellowNote): adapt to web page structure changes (#10235)
* fix(YellowNote): adapt to web page structure changes

* feat(YellowNote): make adjustments according to the reviewer's suggestions

- use stable value to pase date string
- inline selector
- combine two operations into one using mapIndexed()

* fix(YellowNote): correct image selector

* fix(YellowNote): correct data parse

* fix(YellowNote): correct data parse

* fix(YellowNote): properly adapt to new languages

- Implement correct language adaptation
- Add settings for language selection, defaulting to system language if unset
- Use English for unsupported languages
- Fix incorrect formatMediaCount extraction

* fix(YellowNote): update date parsing logic from version info

* chore(YellowNote): remove log

* chore(YellowNote): remove unused multilingual content

* fix(YellowNote): optimize Chinese language tag logic

- Simplify Chinese language tag conditions
- Add support for Simplified Chinese in Singapore (SG) region
- Fix potential incorrect default language tagging

* fix(YellowNote): override id

* feat(YellowNote): add language switch notification and optimize config

- Add success notification for language switching
- Remove redundant getStringOrDefault implementation

* fix(YellowNote): use tryParse
2025-10-03 00:00:22 +01:00
Smol Ame
2fd2613bd0
Remove Manga1st.online (#10733) 2025-10-03 00:00:22 +01:00
Smol Ame
2259f1bc8b
ManhuaTop: Fix Browse (#10732)
* ManhuaTop: Bump versionCode

* ManhuaTop: Set useNewChapterEndpoint to true

* Manhua Top: Update popularManga selectors

---------

Co-authored-by: Secozzi <49240133+secozzi@users.noreply.github.com>
2025-10-03 00:00:22 +01:00
Smol Ame
c0129e532f
Arab Toons: Small fixes (#10724)
* Arab Toons: Bump versionCode

* Arab Toons: Update baseUrl

* Arab Toons: Update dateFormat

* Arab Toons: Remove useNewChapterEndpoint override

* Arab Toons: Fix applicable mangaDetailsSelector

* Arab Toons: Un-update baseUrl
2025-10-03 00:00:22 +01:00
mrtear
d3b715a4be
Add SanaScans (#10722)
sanascans
2025-10-03 00:00:22 +01:00
mrtear
6d63d49b16
Add RageScans (#10721)
ragescans
2025-10-03 00:00:22 +01:00
marioplus
728dd0de50
fix(buondua): handle single-page URL parsing case (#10714)
- Fix URL extraction when only one page exists
2025-10-03 00:00:22 +01:00
Chopper
ff7a95faa0
SeitaCelestial: Theme changed (#10711)
Theme changed
2025-10-03 00:00:22 +01:00
bapeey
e70acec541
Atsumaru: Use new API (#10705)
* update api

* add manga type to genre

* oops

* show type first
2025-10-03 00:00:22 +01:00
Chopper
d824aa0a17
SakuraMangas: Fix loading content (#10685)
* Fix loading content

* Remove @RequiresApi
2025-10-03 00:00:22 +01:00
Vetle Ledaal
d22070e11c
Sagrado Império da Britannia: fix search (#10684) 2025-10-03 00:00:22 +01:00
Vetle Ledaal
3d97ceb3eb
Taiyō: use correct domain in UrlActivity comments (#10663) 2025-10-03 00:00:22 +01:00
Vetle Ledaal
9b49a7d4f1
Manga TV: decode image links, skip redirect (#10661)
* Manga TV: decode image links, skip redirect

* fix basic search
2025-10-03 00:00:22 +01:00
Vetle Ledaal
1e32eb651c
HentaiFantasy: use non-www domain, update tags (#10660) 2025-10-03 00:00:22 +01:00
Smol Ame
09ebe226b7
Hiperdex: Fix chapter date parsing (#10673)
* Hiperdex: Bump versionCode

* Hiperdex: Override dateFormat

* Hiperdex: Update baseUrl
2025-10-03 00:00:22 +01:00
Jake
af9e1adb51
Mangabox: Fix Selectors (#10702) 2025-10-03 00:00:22 +01:00
bapeey
1905a3c8dc
Remove Cartel de Manhwas (#10666)
dead source
2025-10-03 00:00:20 +01:00
Chopper
2a5d28df53
Update domain (#10659)
* Update domain

* Remove regex

* Remove banner

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-10-03 00:00:20 +01:00
mrtear
a687048749
Add GalaxyManga[en] & Remove dead Galaxy (multi) (#10654)
* Delete Galaxy (multi)

* GalaxyManga
2025-10-03 00:00:20 +01:00
mrtear
e2895bad8e
Added BaekToons, LevaScans, RizzComic (unoriginal) (#10652)
* baektoons

* levascans

* rizzcomicun (original)
2025-10-03 00:00:20 +01:00
Justin COLLON
250ab83b35
Japscan fix (#10651)
Japscan:
 - fix manhua & manhwa url
 - better error management
2025-10-03 00:00:20 +01:00
mrtear
588b4d8bf4
Add Razure (#10639)
* razure

* oops

* manga selector
2025-10-03 00:00:20 +01:00
Chopper
27d5cf14b3
PlumaComics: Theme changed (#10649)
Theme changed
2025-10-03 00:00:20 +01:00
Chopper
dc999bacca
Manhuarm: Fix settings (#10648)
Fix settings
2025-10-03 00:00:20 +01:00
Chopper
b90af30496
LerToons: Update domain (#10645)
Update domain
2025-10-03 00:00:19 +01:00
mrtear
6501c051aa
Add NikaToons (#10642)
nikatoons
2025-10-03 00:00:19 +01:00
mrtear
fca279bb58
Add Iken Sources: Diva Scans, Hijala Scans, Vanilla Scans (#10638)
* divascans

* hijalascans

* vanillascans
2025-10-03 00:00:19 +01:00
Smol Ame
d7620c1576
Pluto Scans: Changed to FlameScans.lol (#10631)
* Pluto Scans: Bump versionCode

* Pluto Scans: Bump versionCode

* Pluto Scans: Update baseUrl

* Pluto Scans: Remove unnecessary `SimpleDateFormat`

* Pluto Scans: Update genreList

* Pluto Scans: Update extension name & add `id` override

* Pluto Scans: Rename classes and whatever

* FlameScans.lol: Replace icons
2025-10-03 00:00:19 +01:00
mrtear
846d2d92bd
KiraScans & VioletScans: filter out paid chapters (#10630)
* bump

* update

* VioletScans
2025-10-03 00:00:19 +01:00
mrtear
ea0a3fded0
Add LunaToons (#10629)
* lunatoons

* nice

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

* f

* f2

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-10-03 00:00:19 +01:00
Secozzi
2d450add12
Add kagane (#10599)
* Add kagane

* Small code cleanup

* Make sure nsfw cookie is always added

* Add interceptor to automatically refresh token

* Small code cleanup pt. 2
2025-10-03 00:00:19 +01:00
AlphaBoom
c47e2c1024
MangaGun: Fix page list parse and update domain (#10593)
* MangaGun: Fix page list parse and update domain

* MangaGun: add comments.

* MangaGun: Update icon and source name.

* Update src/ja/mangagun/src/eu/kanade/tachiyomi/extension/ja/mangagun/MangaGun.kt

Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>

* Update extension name

---------

Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>
2025-10-03 00:00:19 +01:00
Aurel
f1f374f773
Fix StarboundScans revert to old loading by using src attribute (#10575)
* fix(starboundscans): revert to old src because of site

* Fix Review
2025-10-03 00:00:19 +01:00
manti
84da2bf59f
Add Azuki (#10543)
* Add Azuki

* nsfw

* lint

* improvements

* rm null

* simplify
2025-10-03 00:00:19 +01:00
mrtear
a45d427b92
Add Noxen Scans (#10628)
noxenscans
2025-10-03 00:00:19 +01:00
thu2468
7b843a9396
Add Qi Scans (#10596)
* Qi Scans

* Update src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/en/qiscans/build.gradle

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt

Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>

* Update QiScans.kt

---------

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>
2025-10-03 00:00:19 +01:00
are-are-are
8da4bc0fd0
TruyenGG: Change name to FoxTruyen & update icon (#10561)
* TruyenGG: Rename to FoxTruyen & update icon

* bump version
2025-10-03 00:00:19 +01:00
Smol Ame
384206b02e
Asura Scans: Rate tweak (#10626)
* Asura Scans: Bump versionCode

* Asura Scans: Adjust rateLimit
2025-10-03 00:00:19 +01:00
Chopper
12b907aee8
Manhuarm: Fix dialog box empty and add translation support (#10625)
Fix dialog box empty and add translation support
2025-10-03 00:00:19 +01:00
Smol Ame
2580c2392d
HentaiRead: Add Western category support (#10620)
* HentaiRead: Bump versionCode

* HentaiRead: Add `Western` category
2025-10-03 00:00:19 +01:00
Chopper
49fd5387e9
CovenScan: Update domain (#10610)
Update domain
2025-10-03 00:00:19 +01:00
Chopper
66ac931044
GalinhaSamuraiScan: Migrate from Yuyu to Madara (#10609)
Migrate from Yuyu to Madara
2025-10-03 00:00:19 +01:00
mrtear
212a442610
Add Siren Scans (#10608)
* sirenscans

* 0
2025-10-03 00:00:19 +01:00
mrtear
d42a931714
Add Grim Scans (#10607)
* grimscans

* 0
2025-10-03 00:00:19 +01:00
Luqman
69d2d65fed
Add Source Elf Toon (#10605)
Add Elf Toon
2025-10-03 00:00:19 +01:00
Luqman
344b380d65
removing Quantum Toon, NIGHT SCANS (#10602) 2025-10-03 00:00:19 +01:00
Smol Ame
95e79f8690
Ikigai Mangas: Update baseUrl (#10601)
* Ikigai Mangas: Bump versionCode

* Ikigai Mangas: Update baseUrl
2025-10-03 00:00:19 +01:00
AwkwardPeak7
23543e9fb8
Remove Comick (#10571) 2025-10-03 00:00:19 +01:00
are-are-are
955b86567b
Remove Fecomic (#10565)
Dead Source
2025-10-03 00:00:19 +01:00
are-are-are
3835cb30f5
Remove NhatTruyenS (#10564)
Dead Source
2025-10-03 00:00:19 +01:00
are-are-are
9c0fefbd7f
Manhuarock: Update domain (#10563) 2025-10-03 00:00:19 +01:00
are-are-are
ba2f0c6271
ManhwaZ: Use ignore case when comparing in status (#10562)
ManhwaZ: Use ignore case parsing when comparing in status
2025-10-03 00:00:19 +01:00
zhongfly
128c4a4194
Zaimanhua: fix authIntercept bug (#10533)
When the request header changes, the new request should not use outdate cache.

Also, clear the token in `apiHeaders` when username or password changed.
2025-10-03 00:00:19 +01:00
manti
a7408115ed
Add K Manga (#10498)
* Add K Manga

* apply fixes, different and consistent thumbnail, better search

* simplify

* manifest, deeplink

* exception message
2025-10-03 00:00:19 +01:00
Romain
b9c1a7afca
fix(fr/scanmanga): Update to the latest stable chrome wv UA (#10550)
Update to the latest stable chrome webview.
2025-10-03 00:00:19 +01:00
Smol Ame
c25f0e4c0f
Remove 1Manhwa (#10549) 2025-10-03 00:00:19 +01:00
are-are-are
12ac787f8f
ManhwaZ: add word for completedStatusList (#10537) 2025-10-03 00:00:19 +01:00
are-are-are
64990abd92
Madara: add wordset for parseRelativeDate (#10536) 2025-10-03 00:00:19 +01:00
are-are-are
96c72c7029
TruyenTranhDamMy: Fix install failed in Suwayomi (#10535) 2025-10-03 00:00:19 +01:00
Smol Ame
c6680c6559
Mangamo: Bump versionCode (#10534)
Bump Mangamo
2025-10-03 00:00:19 +01:00
Aurel
f2d12068a0
Fix StarboundScans image loading by using data-src attribute (#10532)
Fix StarboundScans image loading by using data-src instead of src

- Update pageListParse to use data-src attribute for image URLs
- This fixes the IllegalArgumentException caused by data: URLs
- Aligns with Madara base implementation pattern
2025-10-03 00:00:19 +01:00
Slayer
75e1c1016a
[ES][Madara] Add DragonTranslation.org (#10508)
* [ES][Madara] Add DragonTranslation.org (new source, unrelated to .net)

* Update src/es/dragontranslationorg/build.gradle

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/es/dragontranslationorg/src/eu/kanade/tachiyomi/extension/es/dragontranslationorg/DragonTranslationOrg.kt

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/es/dragontranslationorg/src/eu/kanade/tachiyomi/extension/es/dragontranslationorg/DragonTranslationOrg.kt

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/es/dragontranslationorg/build.gradle

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Update src/es/dragontranslationorg/src/eu/kanade/tachiyomi/extension/es/dragontranslationorg/DragonTranslationOrg.kt

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Remove useNewChapterEndpoint as it is not used

---------

Co-authored-by: Slayer <junioreduca04@gmail.com>
Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
2025-10-03 00:00:19 +01:00
Smol Ame
69ae2a2373
Arven Scans: Update theme to Madara (#10484)
* Arven Scans: Bump versionCode

* Arven Scans: Switch to Madara theme

* Arven Scans: Downbump versionCode to line up with versioning

* Arven Scans: Override mangaSubString

* Arven Scans: Bump versionId

* Arven Scans: Override extension ID

* Arven Scans: Un-override extension ID

* Arven Scans: `useNewChapterEndpoint` and add `rateLimit`
2025-10-03 00:00:19 +01:00
Aurel
901630551a
Webtoons: Option to use sequential chapter numbers + french regex fix (#10451)
* fix(Webtoons): update chapter number handling

* update extVersionCode to 52

* revert to old ver to add 2 options

* Update src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt
2025-10-03 00:00:19 +01:00
Dr1ks
8ad92cc053
LibGroup: update api mirrors and domain (#10519) 2025-10-03 00:00:16 +01:00
Creepler13
585e4e3b53
FlameComis: Fix altTitles (#10511) 2025-10-03 00:00:16 +01:00
Prem Kumar
ea50657f8a
Remove Altay Scans (#10507) 2025-10-03 00:00:16 +01:00
Korodjouma Junior Fofana Coulibaly
b1adb8d810
fix(es-manhuaonline): update domain to samuraiscan.com (#10501)
fix(es-manhuaonline): update baseUrl to samuraiscan.com; bump version code
2025-10-03 00:00:16 +01:00
Smol Ame
347ef46ffc
Remove Crystal Comics (#10497) 2025-10-03 00:00:16 +01:00
are-are-are
acbd615590
VlogTruyen: Update domain (#10494) 2025-10-03 00:00:16 +01:00
are-are-are
04493e878a
Truyenhentai18: Add chapter title (#10493) 2025-10-03 00:00:16 +01:00
are-are-are
8fded6cbf7
LxHentai: Update List Genre and add rate limit (#10491) 2025-10-03 00:00:16 +01:00
Chopper
ae0992fc73
LerToons: Update domain (#10490)
Update domain
2025-10-03 00:00:16 +01:00
Draken
325905f741
GocTruyenTranhVui: Update token, fix chapter list bug (#10488)
* Update token

* Update versionCode
2025-10-03 00:00:16 +01:00
Smol Ame
582848455c
Manga Pro: Fix API URL typo (#10486)
* Manga Pro: Bump versionCode

* Manga Pro: Fix API URL typo
2025-10-03 00:00:16 +01:00
Chopper
f3835ed243
CeriseScan: Theme changed (#10483) 2025-10-03 00:00:16 +01:00
Smol Ame
5dc3fc0cf2
Vortex Scans: Fix Popular & update Latest tabs (#10481)
* Vortex Scans: Bump versionCode

* Vortex Scans: Use API for Popular request

* Vortex Scans: Override Latest request with site's "new" tag query

* Apply suggestion

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>

* Vortex Scans: Parity suggestion onto Popular

---------

Co-authored-by: bapeey <90949336+bapeey@users.noreply.github.com>
2025-10-03 00:00:16 +01:00
KirinRaikage
87647ac04f
Add Kiwiya Scans (#10477)
* Add Kiwiya Scans

* Add missing trailing comma
2025-10-03 00:00:16 +01:00
Dr1ks
3829a2df51
Desu: update domain (#10470) 2025-10-03 00:00:16 +01:00
Dr1ks
9cb0134130
Grouple: fix pages, update domains (#10469)
* Grouple: fix pages

* Grouple: update domains

* Grouple: apply suggestion
2025-10-03 00:00:16 +01:00
MikeZeDev
b39f94faae
Remove dead sources (#10452) 2025-10-03 00:00:16 +01:00
stevenyomi
adbc96dfea
Roumanwu: fix pages (#10441) 2025-10-03 00:00:16 +01:00
AwkwardPeak7
361f7e2c92
webtoons.com: add chapter number to name (#10425)
* webtoons.com: add chapter number to name

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

* remove unused

* Update src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-03 00:00:16 +01:00
manti
be6eab070b
Add Nyanu Kafe (#10424) 2025-10-03 00:00:16 +01:00
Chopper
8a84ce8d8b
Remove MTL (#10408) 2025-10-03 00:00:11 +01:00
Romain
859d5c5007
fix(fr/ScanManga): fix detection (#10373)
fix(ScanManga): fix detection by Accept-Language

Now, it is the same one as chrome :)
2025-10-02 23:59:09 +01:00
Aurel
076f114469
refactor(StarboundScans): migrate to Madara theme after site UI update (#10382) 2025-10-02 23:59:09 +01:00
Smol Ame
96f2aa0344
Manga Pro: Use API URL (#10399)
* Manga Pro: Bump versionCode

* Manga Pro: Add API URL
2025-10-02 23:59:09 +01:00
Smol Ame
c1d6d93ba8
Remove Holymanga (#10398) 2025-10-02 23:59:09 +01:00
Smol Ame
2185b445f2
Remove LemonFont (#10397) 2025-10-02 23:59:09 +01:00
Aurel
b9dd8b2de4
Fix PoseidonScans missing premium chapters and volume detection (#10389)
* feat(PoseidonScans): enhance chapter filtering logic for premium chapters & add Volume parsing

* Correct indentation

* refactor(PoseidonScans): fix reviews
2025-10-02 23:59:09 +01:00
manti
a999e665de
Quantum Scans/Toon: Redesign (#10368)
* Quantum Toon Redesign

* separate dto

* fix genre and robust page parsing

* rsc: 1

* try block
2025-10-02 23:59:09 +01:00
manti
eb51001d0a
Add Kirascans (#10378)
* Add Kirascans

* rm checks
2025-10-02 23:59:09 +01:00
bapeey
9dcb904c21
LectorJPG: Update theme (#10367)
* update theme

* update logo

* review changes
2025-10-02 23:59:09 +01:00
stevenyomi
df9da07535
Update Komiic (#10376)
- Refresh token automatically
- Refactor requests
- Fetch genres from website
- Tweak chapter list
2025-10-02 23:59:09 +01:00
renovate[bot]
3e65d19929
Update dependency com.android.tools.build:gradle to v8.13.0 (#10385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 23:59:08 +01:00
zhongfly
105e329c47
Zaimanhua: add genre filter & check token expiration (#10357)
* Zaimanhua: make comments list immutable

* Zaimanhua: add genre filter

Also refactors the ranking filter to allow disabling it.

* Zaimanhua: check JWT token expiration

* Zaimanhua: use parseAs functions from utils

* misc
2025-10-02 23:59:08 +01:00
Ananda Umamil
0fae25ac43
fix(Madara): preserve the capitalization of tags and genres (#10353)
Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:59:08 +01:00
Luqman
08cf9260e8
ReYume: fix issue from changing site theme (#10279) 2025-10-02 23:59:08 +01:00
stevenyomi
9113f87e1e
Strip all AGP version data from APKs (#10355) 2025-10-02 23:59:08 +01:00
renovate[bot]
321cfbee03
Update dependency com.android.tools.build:gradle to v8.12.2 (#10343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 23:59:08 +01:00
stevenyomi
3e14b9b697
Fix Dm5 chapters and pages (#10341)
* Fix Dm5 chapters and pages

* desktop user agent

* clear more cookies

* clear all cookies
2025-10-02 23:59:08 +01:00
zhongfly
14b5edc771
Zaimanhua: Improve comment rendering (#10340)
- Better text layout and spacing for comments.
- Ensure the generated image is not too short, to avoid being displayed as a double-page spread.
2025-10-02 23:59:08 +01:00
Chopper
314b8f3848
FenixManhwas: Redesign (#10336) 2025-10-02 23:59:08 +01:00
manti
0defe7773b
Add Wearehunger (#10329) 2025-10-02 23:59:08 +01:00
manti
a62e17736c
Add RDScans (#10327) 2025-10-02 23:59:08 +01:00
manti
efe09f539b
Fix Philiascans (#10322) 2025-10-02 23:59:08 +01:00
Alan Tan
79bdda34b2
Goda (baozimhorg): Update api url (#10299)
* baozimhorg: Update api url

* baozimhorg: fix unparseable date

* baozimhorg: version bump

* baozimhorg: fix some chapter is missing

api v3 return different result depending if there is origin or not

* baozimhorg: cleanup

* baozimhorg: update parseDate function

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

* baozimhorg: update parseDate function

* Update headers

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

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-10-02 23:59:08 +01:00
solkaz
b6bce67308
smbc: add extension for smbc-comics.com (#10192)
* smbc: add extension for smbc-comics.com

Adds an extension for Saturday Morning Breakfast Comics

* hiveworks: remove references to Saturday Morning Breakfast Comics

Removes code that was made to handle reading SMBC specifically. If a
user still has the comic in the Hiveworks extension, they'll get a
warning to migrate to the SMBC extension.
2025-10-02 23:59:08 +01:00
stevenyomi
804fd752e8
Fix versionId being deleted in index 2025-10-02 23:59:08 +01:00
tanaka-shizuku3
af70c4b12c
Sixmh: Fix search (#10331)
* Sixmh: Fix search

* Add comment

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

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:59:08 +01:00
tanaka-shizuku3
ef7a5f6faa
Dm5: Add mirror selection in preference (#10330)
* Dm5: Add mirror selection in preference

* Add MIRROR_PREF constant
2025-10-02 23:59:08 +01:00
zhongfly
6c255f4658
Zaimanhua: Fix login check & add chapter comments (#10328)
* Zaimanhua: fix login check

When the token expires, accessing restricted chapters still returns errno=0. Therefore, the approach has been changed to check whether canRead is false.

* Zaimanhua: Use constants for preference keys

* Zaimanhua: Add chapter comments

This commit adds a feature to display comments at the end of each chapter.

- Added a new `CommentDataDto` and a custom `LastStringFromArrayListSerializer` to handle the comment data structure.
- Implemented a `commentsInterceptor` to fetch and render comments as an image.
- Added a preference option to enable/disable chapter comments.
- Updated `fetchPageList` to include the comment page if the preference is enabled.
- Modified `imageRequest` to handle comment page requests.
- Added helper functions `chapterCommentsUrl` and `parseChapterComments`.

* Zaimanhua: tag image requests
2025-10-02 23:59:08 +01:00
Romain
e49d76ff14
fix(ScanManga): fix regex expression (#10326)
Fix regex expression
Added some error to easily pinpoint where it fails when it fails.

Closes #10317
2025-10-02 23:59:08 +01:00
SupKelelawar
196b76805c
KomikindoID: fix next page (#10312)
* KomikindoID: next page problem (#10301)

* Update build.gradle
2025-10-02 23:59:08 +01:00
Chopper
7dea021357
MachineTranslations: Add font settings (#10307)
* Add font settings

* Bump version

* Fix utf-8

* Add open fonts

* Fix lint

* Update messages

* Fix filename

* Remove distinctBy
2025-10-02 23:59:08 +01:00
Hualiang
f990ba581a
BiliManga: use markdown in description (#10304)
use markdown in description
2025-10-02 23:59:08 +01:00
are-are-are
76c7d2a0a7
MiMiHentai: Update search support exclude tag and remove 'yaoi' tag from Latest/Popular page (#10277)
* Update search and Remove yaoi tag from latest and popular page

People suggest removing this tag from newest and most popular pages and support to remove the tag when the web allows it

* Update pageListParse & PageListDto
2025-10-02 23:59:08 +01:00
stevenyomi
0d51803f91
Reinstate versionId field in index.json (#10293)
* Reinstate versionId field in index.json

* address deprecation
2025-10-02 23:59:08 +01:00
stevenyomi
b59f8b5c53
Ikiru: fix chapter order (#10272) 2025-10-02 23:59:04 +01:00
stevenyomi
c514b4fc04
Fix Roumanwu (#10271) 2025-10-02 23:59:04 +01:00
tanaka-shizuku3
c6bad74c45
Add Tongli (#10263) 2025-10-02 23:59:04 +01:00
Luqman
8fe8ca4fd1
Ikiru: rewrite for new site (#10249) 2025-10-02 23:59:04 +01:00
stevenyomi
c796e33925
Iqiyi/Tencent/Dongman: set desktop UA, move to zh-Hans (#10266) 2025-10-02 23:59:04 +01:00
tanaka-shizuku3
c74fe07813
MMLook: Fix search of Dumanwu (#10265) 2025-10-02 23:59:04 +01:00
Genzales6
d1fe20ff09
Cartel de Manhwas - fixed the Search results problem (#10257) 2025-10-02 23:59:04 +01:00
stevenyomi
c0e22429bb
Remove Bilibili Manga and Kuaikanmanhua (#10255) 2025-10-02 23:59:04 +01:00
stevenyomi
0e9e55b945
Use source().asResponseBody() to fix MIME to avoid extra memory load (#10254) 2025-10-02 23:59:04 +01:00
stevenyomi
f50bec002b
Use CipherSource to decrypt responses by streaming (#10253) 2025-10-02 23:59:04 +01:00
morallkat
e4cd4833e0
zh/boylove: add manga region filter (#10252) 2025-10-02 23:59:04 +01:00
stevenyomi
b57f7d72d4
MangaDex: remove MD@Home reporting (#10250) 2025-10-02 23:59:04 +01:00
stevenyomi
8acd1707ae
Remove DMZJ (#10248) 2025-10-02 23:59:04 +01:00
Hiirbaf
70df8cbfa9
Batcave: Add Genres (#10237) 2025-10-02 23:59:04 +01:00
Romain
eb480815e8
Invinciblescans: Trial and error the file extension of images. (#10207) 2025-10-02 23:59:04 +01:00
renovate[bot]
a4babea523
Update dependency com.android.tools.build:gradle to v8.12.1 (#10260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 23:59:04 +01:00
Chopper
1c8cd221fd
Add Monsure (#10241) 2025-10-02 23:59:02 +01:00
Chopper
00ec365e30
Add MRYaoi (#10240) 2025-10-02 23:59:02 +01:00
Chopper
41a275917d
MachineTranslations: Fix bad fragments and settings (#10239)
* Fix bad fragments and settings

* Bump version

* Use keiyoushi.utils.parseAs

* Avoid replacing default settings
2025-10-02 23:59:02 +01:00
Luqman
8059156500
CosmicScans.id: update domain, fix chapter date (#10233) 2025-10-02 23:59:02 +01:00
Luqman
24a6a88c46
KomikIndoID: update domain, fix browse pages (#10232) 2025-10-02 23:59:02 +01:00
kanoou
6d00432dc7
Remove DragonTranslation.net and Manhwas.net (#10226)
remove sites
2025-10-02 23:59:02 +01:00
Chopper
86e982fa3f
Solarmtl/Snowmtl: Add support for Spanish and Italian, and fix settings (#10224)
* Add support for Spanish and Italian

* Bump version

* Fix translator switch

* Add more options for font size

* Fix prefs

* Fix font size default

* Add support for translating manga details
2025-10-02 23:59:02 +01:00
stevenyomi
15cc2c886d
Add workaround buttons for The Blank Scanlation (#10219)
* Add workaround buttons for The Blank Scanlation

* refactor

* Update instructions
2025-10-02 23:59:02 +01:00
Luqman
77b89cec80
MGKomik: fix null pointer error (#10215) 2025-10-02 23:58:59 +01:00
Luqman
d62b06989b
WestManga: fix error 404 (#10214) 2025-10-02 23:58:59 +01:00
Chopper
89beec2f78
Solarmtl: Add support to Indonesian and Arabic (#10213)
* Add support to Indonesian and Arabic

* Add settings to disable translator
2025-10-02 23:58:50 +01:00
Chopper
6011d20b86
Astratoons: Fix pages (#10209)
Fix pages
2025-10-02 23:57:47 +01:00
Romain
bafa8f61ee
Fix epsilonscans "You have been blocked" (#10208) 2025-10-02 23:57:46 +01:00
morallkat
508414951e
zh/boylove: fix unscrambler/filter selectors, Add VIP manga filter (#10206)
zh/boylove: fix unscrambler/filter selectors and add filter entry for VIP manga
2025-10-02 23:57:46 +01:00
Secozzi
510d50ab58
Japscan: Fix chapter list, cloudflare issues, and page list not loading (#10186)
Fix chapter list, cloudflare issues, and page list not loading
2025-10-02 23:57:45 +01:00
David
a5befc4d52
Remove sorting from preferred groups list (#10178)
remove sorting
2025-10-02 23:57:45 +01:00
Secozzi
262d246f31
Japscan: Update url & fix other stuff (#10169)
Update url & other stuff
2025-10-02 23:57:45 +01:00
Hualiang
88407d64af
BiliManga: add more filter options (#10166)
* add more filter options

* fix pagination
2025-10-02 23:57:45 +01:00
tanaka-shizuku3
4587ac2c1d
Add toptoon.net (#10160)
* Add toptoon.net

* Use HttpSource

* Use parseAs from core

* Use SimpleDateFormat.tryParse

* Use fragment for search query

* Add "lock emoji" comment

* Use plain class for dto

* Fix MangaDto

* Fix ThumbnailDto

* Fix name

* Fix extName

* Update comment
2025-10-02 23:57:45 +01:00
Romain
36eb58e893
Add Invincible ComicsVf (#10153)
* Add Invincible ComicsVf

Closes #9956

* Added Icon

* Final details

* Apply recomandations

* Delete src/fr/invinciblecomics/.gitignore

* Set the version to 1 in build.gradle

* Name with some caps
2025-10-02 23:57:45 +01:00
David
8a3758959b
Comick: Add preferred groups filter and it's localization (#10159)
* Add preferred groups filter and it's localization

* stevenyomi changes

* lint error

* remove migrated preferred groups

* ignoredGroups -> ignoredGroupsLowercase
2025-10-02 23:57:45 +01:00
Chopper
3642771ba0
Snowmtl: Add support to French (#10156)
Add support to French
2025-10-02 23:57:45 +01:00
Luqman
79fed76487
Comick: separate small thumbnail rate limit, tweak parse cover (#10154)
Comick: separate thumbnail rate limit, parse cover tweak

now that the thumbnail images on browse is using the small version we can make it use lower rate limit and separate from ch image rate limit

tweaking the parse cover to do better at handling "." after "#" in the thumbnail url
2025-10-02 23:57:45 +01:00
marioplus
2b41b3f29e
fix(bakamh): fix unable to load chapter (#10152) 2025-10-02 23:57:45 +01:00
Bartu Özen
28232ab96a
Fix Temple Scan (#10148) 2025-10-02 23:57:45 +01:00
Hualiang
4bafe8c57e
Bilimanga & XGMN: fix manga url error when opening webview and the issue with page turning (#10146)
* Fix manga url error when opening webview

* fixing the issue with page turning

* update icon

* revert
2025-10-02 23:57:45 +01:00
Romain
672c54a8cc
Update scanmanga (#10135)
* First Commit ScanManga

* Increase extVersionCode

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Update src/fr/scanmanga/src/eu/kanade/tachiyomi/extension/fr/scanmanga/ScanManga.kt

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

* Step one (not working)

* My last attempt

* Update popular, latest, and search functions

* Much more trial and error later

* This is it. It's working :)))

* Cleaned from debuggers

* Apply suggestions from  stevenyomi

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

* More suggestions

* Convert to HttpSource

* Added base image url at the top

* Only disable cookies when absolutely needed.

---------

Co-authored-by: osamu00 <osamu.kozu@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:45 +01:00
Chopper
648bb3c295
Fix ZeroTheme (#10078)
* Fix ZeroTheme

* Throws an exception when old entries are being processed

* Remove url prefix
2025-10-02 23:57:44 +01:00
stevenyomi
9f1f449d96
Fix ColaManga (#10147)
Co-authored-by: Howard Wu <HowardWu20@outlook.com>
2025-10-02 23:57:44 +01:00
stevenyomi
9ea67f22dd
Kemono: fix cache and post list, update domain (#10134)
* Kemono: fix cache and post list, update domain

* update
2025-10-02 23:57:44 +01:00
Genzales6
e61892ced7
Harem de Kira - New Redesign (#10133)
* Harem de Kira - Update theme

Users will have to migrate

Close #6853

* HaremDeKira.kt

* Update HaremDeKira
2025-10-02 23:57:44 +01:00
stevenyomi
32e7639231
Fix debug build error due to #10131 2025-10-02 23:57:44 +01:00
renovate[bot]
04ad4ce15c
Update dependency com.android.tools.build:gradle to v8.12.0 (#10071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
renovate[bot]
1631c87989
Update dependency gradle to v8.14.3 (#9134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
stevenyomi
7c212dfaab
Remove AGP version metadata from generated APKs (#10131) 2025-10-02 23:57:44 +01:00
zhongfly
1e8fec699c
zaimanhua: update MangaDto and PageItemDto to handle nullable fields (#10111)
Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
Howard Wu
3a62d41476
Zaimanhua: Fix get mangaDetails failed for some manga, add reason for restricted manga (#10102)
* Fix get mangaDetails failed for some manga, add reason for restricted manga.

* Bump version to 10

* Update src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt

Co-authored-by: zhongfly <11155705+zhongfly@users.noreply.github.com>

---------

Co-authored-by: zhongfly <11155705+zhongfly@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
Luqman
f6d2fd3c65
Comick: Option cover quality (#10085)
* Comick: Option cover quality

i think need some text suggestion

use cover quality served by the site by default on browse. this can save lot of bandwidth compared to original 2x-100x smaller. thats why i think can increase rate limit  a little bit.

add option to choose cover quality when opening the manga

closes #9088
closes #2550

* update version

* fix first vol cover, renaming

* cleaning, already default value

* applied to first vol cover as well

* change default value

* rename

* Update messages_en.properties
2025-10-02 23:57:44 +01:00
WebDitto
485f66bad8
feat: Added pt/SakuraMangas (close #7946) (#10079)
* feat: Added pt/SakuraMangas (close #7946)

* chore: Fixed some reviews

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
Hasan
f926a7277d
Add: ElderManga (#10057)
* Add: ElderManga

* Update

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

* Update

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

* Update

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

* Refactor ElderManga source to use HttpSource

* refactor(eldermanga): apply review suggestions

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:44 +01:00
Vetle Ledaal
740a11745d
Update ww# subdomain, sync titles (#10089) 2025-10-02 23:57:37 +01:00
AwkwardPeak7
ad871734f3
add Ryumanga (#10082) 2025-10-02 23:57:37 +01:00
AwkwardPeak7
a4967cb732
add HeyToon (#10081) 2025-10-02 23:57:37 +01:00
marioplus
e8fe42b283
fix(BuonDua): resolve issue with loading more than 9 galleries (#10074)
- Use URL of the last page to determine maximum page count

#Closes: #9139
2025-10-02 23:57:37 +01:00
kanoou
4c7f8ebf68
TempleScan(es): Fix domain updater (#10073)
fix domain updater
2025-10-02 23:57:37 +01:00
Hualiang
d514463edd
Add Xgmn Source (#10067)
* init

* complete

* complete

* fix

* apply commit

* move to all
2025-10-02 23:57:37 +01:00
dngonz
f9e6919908
Mangacrab : Fix thumbnail and images (#10061)
* fix thumbnail and images

* bump
2025-10-02 23:57:37 +01:00
Yakoo
8c7c46e0e2
AnimeSama: add filtering by genre (#10059)
* Update AnimeSama

Fix description & genre by updating html tag id

* Update build.gradle

Increasing the code version number

* Adding latest items for AnimeSama

only the items available on the main page

* Fix URL on latest items

* AnimeSama: adding genre filter

Using almost the same code as FuzzyDoodle, I added the only filter available on animesama: genre

* Update AnimeSama.kt

remove lint errors

* Including code review optimization

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

* Fix build error

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:37 +01:00
Hasan
c2317eeeed
Manga-TR: Add filter support and fix the image could not be loaded error (#10055)
* Add filter support and fix the image could not be loaded error

Add dynamic Genres from manga-list.html (#genreSelect), send as repeated genre[]=<value>
Add filters: Publication (durum), Translation (ceviri), Age (yas), Content Type (icerik), Special Type (tur)
Fix WebView images by resolving Base64-encoded data-src with fallbacks

* Apply requested changes
2025-10-02 23:57:37 +01:00
stevenyomi
45f31c3b75
Fix Android Manifest hack for new AGP versions (#10076)
* Fix Android Manifest hack for new AGP versions

* Minor changes
2025-10-02 23:57:37 +01:00
stevenyomi
95e98fd5f1
Renovate: fix include paths to allow AGP updates
It needs to know which repositories we're using.
2025-10-02 23:57:34 +01:00
stevenyomi
a69d3321d5
Renovate: try enabling AGP updates 2025-10-02 23:57:34 +01:00
Genzales6
6b87ca01d3
TempleScanES Domain Change (#10065)
Closes #9833

The automatic domain lookup system is not working.
2025-10-02 23:57:34 +01:00
meatballsaretasty
2f21fcf8f7
Fixhc including related images in gallery (#10064)
* Update build.gradle

* Update HentaiCosplay.kt

Due to HC website changes they now had low res unrelated images appended to the list of images. They will now be excluded from the gallery.
2025-10-02 23:57:34 +01:00
dngonz
7a3053be7c
Mangagun: Fix thumbnail url selector (#10058)
fix thumbnail url selector
2025-10-02 23:57:34 +01:00
Hasan
8d13fbaedc
Koreli Scans: Update base URL to .net domain (#10056)
* Koreli Scans: Update base URL to .net domain

* Update build.gradle
2025-10-02 23:57:34 +01:00
Luqman
8f13e8b15c
Comick: increase rate limit and separate (#10033)
* Comick: increase rate limit and separate

Closes #9057

* separate ratelimit correctly to avoid stacking

* cleaning
2025-10-02 23:57:34 +01:00
dngonz
d8fe748376
Klikmanga: Fix url (#10054)
fix url
2025-10-02 23:57:34 +01:00
dngonz
c73ddac889
CosmicScans: Fix url (#10053)
change url
2025-10-02 23:57:34 +01:00
Genzales6
d49b5560f6
Samurai & KnightNoScan Domain Updates (#10047) 2025-10-02 23:57:34 +01:00
Genzales6
6ffe8e7af9
MiauScan Domain Update (#10044)
MiauScan domine Update
2025-10-02 23:57:34 +01:00
KirinRaikage
8d7a9fc3f0
Perf Scan: Update domain (#10041) 2025-10-02 23:57:34 +01:00
Genzales6
7be0deebd4
Catharsis World Update Domain (#10032)
Closes #10012
2025-10-02 23:57:33 +01:00
David Brochero
c59f75b06b
add: WitchScans (#10031)
* add: WitchScans

* simplify filter list
2025-10-02 23:57:33 +01:00
Luqman
3f9573f621
West Manga: update genre, country/type (#10030)
* West Manga: update genre, country/type

Closes #9898

* fix lint
2025-10-02 23:57:33 +01:00
Smol Ame
2304fdc713
Remove Lunar Scans (EN) (#10022)
Remove Lunar Scans
2025-10-02 23:57:33 +01:00
David Brochero
7b8e6298a7
fix(RokariComics): ignore locked chapters in chapterListSelector (#10019)
* fix: ignore locked in chapter list selector

* add background color to icons

* simplify filter list
2025-10-02 23:57:33 +01:00
Vetle Ledaal
e05b6448bb
Add try.jsoup.org to the contributing guidelines (#10018) 2025-10-02 23:57:33 +01:00
Luqman
5efcdd071e
MGKomik: fix cf in popular and search (#10017)
* MGKomik: fix cf in popular and search

* cleaning

Co-Authored-By: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Update MGKomik.kt

* Update src/id/mgkomik/src/eu/kanade/tachiyomi/extension/id/mgkomik/MGKomik.kt

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

* cleaning

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:33 +01:00
are-are-are
b1ddba5462
Vlogtruyen: Update base URL & improve chapter filtering & use parseAs (#10008)
Update base URL & improve chapter filtering & use parseAs
2025-10-02 23:57:33 +01:00
Alan Tan
4a2c42a901
Nhentai: Fix manga error when login (#10002)
* NHentai: Fix #9890

* NHentai: Version bump
2025-10-02 23:57:33 +01:00
Yakoo
da094790d1
Fix extension Anime-Sama for description & genre (#9990)
* Update AnimeSama

Fix description & genre by updating html tag id

* Update build.gradle

Increasing the code version number

* Adding latest items for AnimeSama

only the items available on the main page

* Fix URL on latest items
2025-10-02 23:57:33 +01:00
RyanWang
734569ace3
Jinman Tiantang: add search filters like author (#9979) 2025-10-02 23:57:33 +01:00
Chopper
31328ee895
ArgosScan: Fix popular manga (#9971)
* Fix popular manga

* Add 'isNsfw'

* Remove destructuring declaration
2025-10-02 23:57:33 +01:00
David Brochero
b32672b47c
add: RokariComics (#9970)
* add: RokariComics

* ref: suggested changes
2025-10-02 23:57:33 +01:00
nicki
26770629cb
MangaPlus: add new label DX (#9977)
* add new label "DX" to M+

currently only includes one title (100535)

* bump ext version
2025-10-02 23:57:33 +01:00
AwkwardPeak7
853a801f2f
NHentai: fix thumbnails (#9961)
* NHentai: fix thumbnails

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

* rename
2025-10-02 23:57:33 +01:00
thatDudo
c2d47af025
Fix weebcentral filter search showing duplicates (#9955)
* Fix weebcentral filter search showing duplicates

* Add comment for FETCH_LIMIT
2025-10-02 23:57:33 +01:00
EmZedace
3d44d9423d
Read One Punch Man: Updated baseUrl (#9929)
* Updated domain of One Punch Man extension

* Added newly added sources in the new website

* Fixed an issue where wrong date was displayed for chapters

* Review changes, fixed correct date not displaying in ReadBerserkManga as well

---------

Co-authored-by: MuhamedZ1 <88522251+MuhamedZ1@users.noreply.github.com>
2025-10-02 23:57:33 +01:00
hatozuki-programmer
286ccd2f53
GigaViewer: Add paginated chapter list parse support (#9911)
* Support for paginated readable products API

* Simplify date handling

* Chapter status labels

* Fix type

* Handle null display_open_at value

* Additional chapter status label

* Mark GigaViewer paginated sources

* Implement requested changes from feedback

* Remove unused fields

* Use tryParse for date handling

* Remove label constants

* Remove extra whitespace
2025-10-02 23:57:33 +01:00
Vetle Ledaal
fd494a9fa7
Remove Romantik Manga (replace with Webtoon Hatti) (#9927) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
847ad8c44f
Komiku.com: update domain (#9926) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
09e1750887
Webtoon Hatti: update domain, date format (#9925) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
bd8174f9e9
Moon Daisy Scans: update domain (#9924) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
554bc6e08c
Yaoi Flix: update domain (#9923) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
2b3b014c2c
Siren Komik: update domain (#9922) 2025-10-02 23:57:33 +01:00
Vetle Ledaal
a3f4af82ea
Tojimangas: update domain (#9921) 2025-10-02 23:57:33 +01:00
Creepler13
658b70b8fe
Kemono: update Url (#9937) 2025-10-02 23:57:33 +01:00
Emiliano Nava
3a3c1e6481
Mangaworld: Update Domain (#9886) 2025-10-02 23:57:33 +01:00
are-are-are
cb86a4ac6e
GocTruyenTranh: Update genre & use parseAs and tryParse (#9877)
Update genre & use parseAs and tryParse
2025-10-02 23:57:33 +01:00
Genzales6
2f54e79818
Biblio Panda Updated Domain (#9866)
BiblioPanda Url Changes
2025-10-02 23:57:33 +01:00
MediocreLegion
a8fad4b5c2
fix nhentai cdn script selection (#9857)
* fix nhentai cdn script selection

* apply suggestions
2025-10-02 23:57:33 +01:00
are-are-are
e18a2f732c
GocTruyenTranhVui: Using the token of a level 1 account. Login is no longer required. (#9855)
* Currently using token of a lv1 account. Will not require login anymore

Helps read most manga, manhua, manhwa. Manga/manhua/manhwa above level 1 will only be read on the web.

* Apply suggestion
2025-10-02 23:57:33 +01:00
whitebeardhelper
ca348c78eb
ManhwasMen: fix sources not loading mangas (#9836)
* ManhwasMen: fix popular manda selectors and creator, latest manga selectors and parser, manga details parser

* manhwasmen: add alternative names in description

* manhwasmen: change .not to isNotBlank

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

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:33 +01:00
kanoou
22913c11e0
EternalMangas: Update domain (#9837)
update domain
2025-10-02 23:57:33 +01:00
Creepler13
41e519046b
JeazScans: change to madara (#9832)
Change JeazScans to madara
2025-10-02 23:57:33 +01:00
whitebeardhelper
346c5b3338
MangaKimi: fix loading pages when reader is from MangaThemesia theme (#9831) 2025-10-02 23:57:33 +01:00
whitebeardhelper
b8bd224804
YugenMangas: update baseUrl (#9828) 2025-10-02 23:57:33 +01:00
whitebeardhelper
c4794638ff
Bakai: fix element typo mangaselector (#9827) 2025-10-02 23:57:33 +01:00
whitebeardhelper
24591d0836
Mitaki: fix no results found error (#9822) 2025-10-02 23:57:33 +01:00
whitebeardhelper
92edcff4a2
bakai: fix no results found (#9821) 2025-10-02 23:57:33 +01:00
whitebeardhelper
17fb3be9b6
Astratoons: fix chapters pages not found (#9820)
Astratoons: fix chapters pages
2025-10-02 23:57:33 +01:00
Vetle Ledaal
e6b34e2b2d
Jiangzaitoon: update domain, fix blurry thumbnails (#9815)
* Jiangzaitoon: update domain, fix blurry thumbnails

Madara was also updated to allow overriding `String.getSrcSetImage()`, not bumping.

* guard against malformed srcset candiate

* remove duplicate trim
2025-10-02 23:57:33 +01:00
Vetle Ledaal
8361f4774f
Komikindo: update baseUrl (#9814) 2025-10-02 23:57:33 +01:00
Chopper
d4fb412f9e
HuntersScans: Fix chapter list (#9809)
* Fix chapter list

* Remove the extra request and the mutable list

* Use a simple loop to retrieve the chapter page
2025-10-02 23:57:33 +01:00
Chopper
9183d5e2d0
SaikaiScan: Update domain (#9807)
* Update domain

* Remove const
2025-10-02 23:57:33 +01:00
Chopper
9755776353
LeitorDeManga: Bypass jschallenge (#9804)
* Fix 403

* Rewrite to an eligible tailrec statement

* Bump version

* Remove use function
2025-10-02 23:57:33 +01:00
Vetle Ledaal
9840ff97d5
Remove dead sources (#9811)
* Remove CAT-translator

* Remove Jellyring

* Remove ManWe

* Remove Yetiskin Ruya Manga

* Remove Mugi Manga

* Remove Deccal Scans

* Remove FactManga

* Remove Komikuzan

* Remove MangaKazani

* Remove Manga One Love

* Remove Free Manhwa

* Remove Minda Fansub

* Remove SeriManga

* Remove ScamberTraslator

* Remove Arabs Doujin

* Remove DragonManga

* Remove KomikRealm

* Remove Magerin

* Remove YonaBar

* Remove Manga Raw.org

* Remove MangaKomi

* Remove DMC Scans

* Remove Disaster Scans
2025-10-02 23:57:33 +01:00
Chopper
7a09f91cb7
AnimeXNovel: Fix chapter list (#9806)
* Fix chapter list

* Close connection in chapterListParse

* throws an error when the chapter configuration cannot be performed
2025-10-02 23:57:32 +01:00
Chopper
dad8bbf372
Manhastro: Fix auth cookie duplicated (#9798)
* Fix auth cookie duplicated

* Remove looper
2025-10-02 23:57:32 +01:00
Vetle Ledaal
e5ab6fbb21
Bato.to: expose all mirrors for CI (#9810)
* Bato.to: expose all mirrors for CI

* move CI check to `getMirrorPref()`

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

* remove proxy variable `mirror`

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

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:22 +01:00
Chopper
0730f24d95
Inkapk: Update mangaSubString (#9808)
Update mangaSubString
2025-10-02 23:57:22 +01:00
Chopper
8cfdedfa7a
ImperioDaBritannia: update rateLimit (#9805)
update rateLimit
2025-10-02 23:57:22 +01:00
Creepler13
f9d0fec4ff
Flamecomics: Fix images (#9802)
* Fix flamecomics images

* Update src/en/flamecomics/src/eu/kanade/tachiyomi/extension/en/flamecomics/FlameComics.kt

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-10-02 23:57:22 +01:00
zhongfly
7326524050
zaimanhua: update scanlator assignment logic & fix webview url (#9801)
* zaimanhua: update scanlator assignment logic

fix #9794

* zaimanhua: fix webview url
2025-10-02 23:57:22 +01:00
Vetle Ledaal
a42f5c0479
Bato.to: randomize auto mirror better (#9797)
* Bato.to: randomize auto mirror better

* simplify implementation

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

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:22 +01:00
are-are-are
ffff87d5a0
Add source GocTruyenTranhVui (#9728)
* Add GocTruyenTranhVui

* Use jsonInstance

* Use parseAs

* Use HttpSource()

* Merge DTO files

* Using chapterListParse and loginRequired

* Fix variable

* Use toManga(), Use toChapter(), fix no chapter

* Fix Url, Works well

* Add Advanced search

* Optimize variable naming & add mangaId cache

* MangaIdCache: Add limit cache

* Apply suggestion

* Fix package declaration, format using Android Studio

* Fix names: use camel case Dto instead of DTO even if it's acronym; add S in to[S]Manga/Chapter

* Use generic ResultDto<T> to replace similar classes

* Inline the typealiases, which are used to demonstrate how to use generics

* More conventional namings

* Change manga url format; override getMangaUrl; fix chapterListParse slug which is definitely not tested; remove useless HTML parse fallback

* Use timestamp value from API instead of parsing string

* Early abort in pageListParse()

* Refactor filters; don't get an empty filter list if argument is empty, it's uselss

* Parse more manga fields from API and set initialized because all fields are filled; fix listing next page; use selectFirst()!!.text() instead of select().text()

* Use search endpoint for latest updates; the home endpoint doesn't provide genres

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:22 +01:00
whitebeardhelper
88749bec9f
Portal Yaoi: update url (#9795)
update portal yaoi url
2025-10-02 23:57:22 +01:00
AwkwardPeak7
25f2ab773d
Ritharscans & ArtLapsa: fix page list selector (#9792)
* Ritharscans: fix page list

* artlapsa: fix page list
2025-10-02 23:57:22 +01:00
whitebeardhelper
8ac14c2917
Hiperdex: Update domain and fix search (#9787)
* update hiperdex domain

* hiperdex: fix search

* hiperdex: use imageFromElement when search for thumbnail

* fix request when filter is applied

* fix search using more specific properties and functions

* remove unnecessary overrides

---------

Co-authored-by: Alex <WhiteBeard Helper>
2025-10-02 23:57:22 +01:00
are-are-are
99a153304d
MiMiHentai: Advanced Search (#9773)
* Advanced Search

* Add if search

* Update src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/MiMiHentai.kt

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

* Fix build

* Apply suggestion

* Complete Advanced Search

* Revert "Complete Advanced Search"

This reverts commit 25a77c5d9be035f64fe5d9e418686830fb41cde8.

* Delete hs_err_pid25072.log

* Advanced Search

* suggestion

* Revert not apply suggestion because Inefficient

* Update MiMiHentai.kt

* Apply suggestion

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2025-10-02 23:57:22 +01:00
are-are-are
7d6df5d918
Batoto: Fix bug baseUrl = "Auto" (#9767)
* Fix batoto baseUrl = auto

* Update src/all/batoto/build.gradle

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

* Update BatoTo.kt

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-10-02 23:57:22 +01:00
1135 changed files with 12865 additions and 10654 deletions

View File

@ -5,7 +5,9 @@
"schedule": ["on sunday"],
"includePaths": [
"buildSrc/gradle/**",
"buildSrc/*.gradle.kts",
"gradle/**",
"*.gradle.kts",
".github/**"
],
"ignoreDeps": ["keiyoushi/issue-moderator-action"],

View File

@ -79,6 +79,7 @@ for apk in REPO_APK_DIR.iterdir():
"lang": source["lang"],
"id": source["id"],
"baseUrl": source["baseUrl"],
"versionId": source["versionId"],
}
)

View File

@ -12,7 +12,7 @@ MULTISRC_LIB_REGEX = re.compile(r"^lib-multisrc/(?P<multisrc>\w+)")
LIB_REGEX = re.compile(r"^lib/(?P<lib>\w+)")
MODULE_REGEX = re.compile(r"^:src:(?P<lang>\w+):(?P<extension>\w+)$")
CORE_FILES_REGEX = re.compile(
r"^(buildSrc/|core/|gradle/|build\.gradle\.kts|common\.gradle|gradle\.properties|settings\.gradle\.kts)"
r"^(buildSrc/|core/|gradle/|build\.gradle\.kts|common\.gradle|gradle\.properties|settings\.gradle\.kts|.github/scripts)"
)
def run_command(command: str) -> str:

View File

@ -22,7 +22,7 @@ for module in to_delete:
shutil.copytree(src=LOCAL_REPO.joinpath("apk"), dst=REMOTE_REPO.joinpath("apk"), dirs_exist_ok = True)
shutil.copytree(src=LOCAL_REPO.joinpath("icon"), dst=REMOTE_REPO.joinpath("icon"), dirs_exist_ok = True)
with REMOTE_REPO.joinpath("index.min.json").open() as remote_index_file:
with REMOTE_REPO.joinpath("index.json").open() as remote_index_file:
remote_index = json.load(remote_index_file)
with LOCAL_REPO.joinpath("index.min.json").open() as local_index_file:
@ -38,6 +38,10 @@ index.sort(key=lambda x: x["pkg"])
with REMOTE_REPO.joinpath("index.json").open("w", encoding="utf-8") as index_file:
json.dump(index, index_file, ensure_ascii=False, indent=2)
for item in index:
for source in item["sources"]:
source.pop("versionId", None)
with REMOTE_REPO.joinpath("index.min.json").open("w", encoding="utf-8") as index_min_file:
json.dump(index, index_min_file, ensure_ascii=False, separators=(",", ":"))

View File

@ -54,6 +54,7 @@ that existing contributors will not actively teach them to you.
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled and a recent version of Tachiyomi installed
- [Icon Generator](https://as280093.github.io/AndroidAssetStudio/icons-launcher.html)
- [Try jsoup](https://try.jsoup.org/)
### Cloning the repository

View File

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

View File

@ -13,9 +13,7 @@ android {
namespace = "eu.kanade.tachiyomi.lib.${project.name}"
buildFeatures {
androidResources = false
}
androidResources.enable = false
}
dependencies {

View File

@ -17,7 +17,7 @@ android {
namespace "eu.kanade.tachiyomi.extension"
sourceSets {
main {
manifest.srcFile "AndroidManifest.xml"
manifest.srcFile layout.buildDirectory.file('tempAndroidManifest.xml')
java.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
@ -105,21 +105,31 @@ dependencies {
compileOnly(libs.bundles.common)
}
tasks.register("copyManifestFile", Copy) {
from 'AndroidManifest.xml'
rename { 'tempAndroidManifest.xml' }
into layout.buildDirectory
}
tasks.register("writeManifestFile") {
dependsOn(copyManifestFile)
doLast {
def manifest = android.sourceSets.getByName("main").manifest
if (!manifest.srcFile.exists()) {
File tempFile = layout.buildDirectory.get().file("tempAndroidManifest.xml").getAsFile()
if (!tempFile.exists()) {
tempFile.withWriter {
it.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
}
}
manifest.srcFile(tempFile.path)
File tempFile = android.sourceSets.getByName('main').manifest.srcFile
if (!tempFile.exists()) {
tempFile.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
}
}
}
afterEvaluate {
tasks.withType(com.android.build.gradle.tasks.PackageAndroidArtifact).configureEach {
// need to be in afterEvaluate to overwrite default value
createdBy = ""
// https://stackoverflow.com/a/77745844
doFirst { appMetadata.asFile.getOrNull()?.write('') }
}
}
preBuild.dependsOn(writeManifestFile, lintKotlin)
if (System.getenv("CI") != "true") {
lintKotlin.dependsOn(formatKotlin)

View File

@ -1,22 +1,22 @@
[versions]
kotlin_version = "1.7.21"
coroutines_version = "1.6.4"
serialization_version = "1.4.0"
kotlin = "1.7.21"
coroutines = "1.6.4"
serialization = "1.4.0"
[libraries]
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.6.1" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.13.0" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
injekt-core = { module = "com.github.null2264.injekt:injekt-core", version = "4135455a2a" }
rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }

View File

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

View File

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

View File

@ -14,6 +14,8 @@ 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 kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@ -34,7 +36,6 @@ import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.math.floor
@ -44,6 +45,7 @@ abstract class GigaViewer(
override val baseUrl: String,
override val lang: String,
private val cdnUrl: String = "",
private val isPaginated: Boolean = false,
) : ParsedHttpSource() {
override val supportsLatest = true
@ -134,7 +136,7 @@ abstract class GigaViewer(
.attr("data-src")
}
override fun chapterListParse(response: Response): List<SChapter> {
protected fun chapterListParseSinglePage(response: Response): List<SChapter> {
val document = response.asJsoup()
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
@ -180,6 +182,61 @@ abstract class GigaViewer(
return chapters
}
protected fun paginatedChaptersRequest(referer: String, aggregateId: String, offset: Int): Response {
val headers = headers.newBuilder()
.set("Referer", referer)
.build()
val apiUrl = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("api")
.addPathSegment("viewer")
.addPathSegment("pagination_readable_products")
.addQueryParameter("type", "episode")
.addQueryParameter("aggregate_id", aggregateId)
.addQueryParameter("sort_order", "desc")
.addQueryParameter("offset", offset.toString())
.build()
.toString()
val request = GET(apiUrl, headers)
return client.newCall(request).execute()
}
protected fun chapterListParsePaginated(response: Response): List<SChapter> {
val document = response.asJsoup()
val referer = response.request.url.toString()
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
val chapters = mutableListOf<SChapter>()
var offset = 0
// repeat until the offset is too large to return any chapters, resulting in an empty list
while (true) {
// make request
val result = paginatedChaptersRequest(referer, aggregateId, offset)
val resultData = result.parseAs<List<GigaViewerPaginationReadableProduct>>()
if (resultData.isEmpty()) {
break
}
resultData.mapTo(chapters) { element ->
element.toSChapter(chapterListMode, publisher)
}
// increase offset
offset += resultData.size
}
return chapters
}
override fun chapterListParse(response: Response): List<SChapter> {
return if (isPaginated) {
chapterListParsePaginated(response)
} else {
chapterListParseSinglePage(response)
}
}
override fun chapterListSelector() = "li.episode"
protected open val chapterListMode = CHAPTER_LIST_PAID
@ -195,9 +252,7 @@ abstract class GigaViewer(
} else if (chapterListMode == CHAPTER_LIST_LOCKED && element.hasClass("private")) {
name = LOCK + name
}
date_upload = info.selectFirst("span.series-episode-list-date")
?.text().orEmpty()
.toDate()
date_upload = DATE_PARSER_SIMPLE.tryParse(info.selectFirst("span.series-episode-list-date")?.text().orEmpty())
scanlator = publisher
setUrlWithoutDomain(if (info.tagName() == "a") info.attr("href") else mangaUrl)
}
@ -214,13 +269,18 @@ abstract class GigaViewer(
}
}
val isScrambled = episode.readableProduct.pageStructure.choJuGiga == "baku"
return episode.readableProduct.pageStructure.pages
.filter { it.type == "main" }
.mapIndexed { i, page ->
val imageUrl = page.src.toHttpUrl().newBuilder()
.addQueryParameter("width", page.width.toString())
.addQueryParameter("height", page.height.toString())
.toString()
val imageUrl = page.src.toHttpUrl().newBuilder().apply {
addQueryParameter("width", page.width.toString())
addQueryParameter("height", page.height.toString())
if (isScrambled) {
addQueryParameter("baku", "true")
}
}.toString()
Page(i, document.location(), imageUrl)
}
}
@ -254,7 +314,7 @@ abstract class GigaViewer(
protected open fun imageIntercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!request.url.toString().startsWith(cdnUrl)) {
if (!request.url.toString().startsWith(cdnUrl) || request.url.queryParameter("baku") != "true") {
return chain.proceed(request)
}
@ -264,6 +324,7 @@ abstract class GigaViewer(
val newUrl = request.url.newBuilder()
.removeAllQueryParameters("width")
.removeAllQueryParameters("height")
.removeAllQueryParameters("baku")
.build()
request = request.newBuilder().url(newUrl).build()
@ -314,14 +375,7 @@ abstract class GigaViewer(
}
}
private fun String.toDate(): Long {
return runCatching { DATE_PARSER.parse(this)?.time }
.getOrNull() ?: 0L
}
companion object {
private val DATE_PARSER by lazy { SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH) }
private const val DIVIDE_NUM = 4
private const val MULTIPLE = 8
private val jpegMediaType = "image/jpeg".toMediaType()
@ -329,7 +383,7 @@ abstract class GigaViewer(
const val CHAPTER_LIST_PAID = 0
const val CHAPTER_LIST_LOCKED = 1
private const val YEN_BANKNOTE = "💴 "
private const val LOCK = "🔒 "
const val YEN_BANKNOTE = "💴 "
const val LOCK = "🔒 "
}
}

View File

@ -1,6 +1,14 @@
package eu.kanade.tachiyomi.multisrc.gigaviewer
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_LOCKED
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_PAID
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.LOCK
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.YEN_BANKNOTE
import eu.kanade.tachiyomi.source.model.SChapter
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
data class GigaViewerEpisodeDto(
@ -15,6 +23,7 @@ data class GigaViewerReadableProduct(
@Serializable
data class GigaViewerPageStructure(
val pages: List<GigaViewerPage> = emptyList(),
val choJuGiga: String,
)
@Serializable
@ -24,3 +33,31 @@ data class GigaViewerPage(
val type: String = "",
val width: Int = 0,
)
@Serializable
class GigaViewerPaginationReadableProduct(
private val display_open_at: String?,
private val readable_product_id: String = "",
private val status: GigaViewerPaginationReadableProductStatus?,
private val title: String = "",
) {
fun toSChapter(chapterListMode: Int, publisher: String) = SChapter.create().apply {
name = title
if (chapterListMode == CHAPTER_LIST_PAID && status?.label != "is_free") {
name = YEN_BANKNOTE + name
} else if (chapterListMode == CHAPTER_LIST_LOCKED && status?.label == "unpublished") {
name = LOCK + name
}
date_upload = DATE_PARSER_COMPLEX.tryParse(display_open_at)
scanlator = publisher
url = "/episode/$readable_product_id"
}
}
@Serializable
class GigaViewerPaginationReadableProductStatus(
val label: String?, // is_free, is_rentable, is_purchasable, unpublished
)
val DATE_PARSER_SIMPLE = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH)
val DATE_PARSER_COMPLEX = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)

View File

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

View File

@ -373,8 +373,8 @@ abstract class GroupLe(
}
val readerMark = when {
html.contains("rm_h.readerDoInit([") -> "rm_h.readerDoInit(["
html.contains("rm_h.readerInit([") -> "rm_h.readerInit(["
html.contains("rm_h.readerInit(") -> "rm_h.readerInit("
html.contains("rm_h.readerDoInit(") -> "rm_h.readerDoInit("
!response.request.url.toString().contains(baseUrl) -> {
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
}

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 22
baseVersionCode = 23
dependencies {
compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11")
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.kemono
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -15,14 +16,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferences
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import keiyoushi.utils.parseAs
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Request
import okhttp3.Response
import okhttp3.brotli.BrotliInterceptor
import rx.Observable
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.lang.Thread.sleep
import java.util.TimeZone
import java.util.concurrent.TimeUnit
import kotlin.math.min
open class Kemono(
@ -32,13 +38,35 @@ open class Kemono(
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.addInterceptor { chain ->
val request = chain.request()
if (request.url.pathSegments.first() == "api") {
chain.proceed(request.newBuilder().header("Accept", "text/css").build())
} else {
chain.proceed(request)
}
}
.apply {
val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor }
if (index >= 0) interceptors().add(networkInterceptors().removeAt(index))
}
.cache(
Cache(
directory = File(Injekt.get<Application>().externalCacheDir, "network_cache_${name.lowercase()}"),
maxSize = 50L * 1024 * 1024, // 50 MiB
),
)
.build()
private val creatorsClient = client.newBuilder()
.readTimeout(5, TimeUnit.MINUTES)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val preferences = getPreferences()
private val apiPath = "api/v1"
@ -47,8 +75,6 @@ open class Kemono(
private val imgCdnUrl = baseUrl.replace("//", "//img.")
private var mangasCache: List<KemonoCreatorDto> = emptyList()
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
@ -85,6 +111,7 @@ open class Kemono(
is SortFilter -> {
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc"
}
is TypeFilter -> {
filter.state.filter { state -> state.isIncluded() }.forEach { tri ->
typeIncluded.add(tri.value)
@ -94,44 +121,60 @@ open class Kemono(
typeExcluded.add(tri.value)
}
}
is FavouritesFilter -> {
is FavoritesFilter -> {
fav = when (filter.state[0].state) {
0 -> null
1 -> true
else -> false
}
}
else -> {}
}
}
var mangas = mangasCache
if (page == 1 || mangasCache.isEmpty()) {
var favourites: List<KemonoFavouritesDto> = emptyList()
if (fav != null) {
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
val mangas = run {
val favorites = if (fav != null) {
val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
if (favores.code == 401) throw Exception("You are not Logged In")
favourites = favores.parseAs<List<KemonoFavouritesDto>>().filterNot { it.service.lowercase() == "discord" }
if (response.isSuccessful) {
response.parseAs<List<KemonoFavoritesDto>>().filterNot { it.service.lowercase() == "discord" }
} else {
response.close()
val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}"
throw Exception("Failed to fetch favorites: $message")
}
} else {
emptyList()
}
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
val request = GET(
"$baseUrl/$apiPath/creators",
headers,
CacheControl.Builder().maxStale(30, TimeUnit.MINUTES).build(),
)
val response = creatorsClient.newCall(request).execute()
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}")
}
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
mangas = allCreators.filter {
allCreators.filter {
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
val regularSearch = it.name.contains(title, true)
val isFavourited = when (fav) {
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
false -> favourites.none { f -> f.id == it.id }
val isFavorited = when (fav) {
true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
false -> favorites.none { f -> f.id == it.id }
else -> true
}
includeType && !excludeType && isFavourited &&
includeType && !excludeType && isFavorited &&
regularSearch
}.also { mangasCache = it }
}
}
val sorted = when (sort.first) {
@ -142,6 +185,7 @@ open class Kemono(
mangas.sortedBy { it.favorited }
}
}
"tit" -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.name }
@ -149,6 +193,7 @@ open class Kemono(
mangas.sortedBy { it.name }
}
}
"new" -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.id }
@ -156,14 +201,16 @@ open class Kemono(
mangas.sortedBy { it.id }
}
}
"fav" -> {
if (fav != true) throw Exception("Please check 'Favourites Only' Filter")
if (fav != true) throw Exception("Please check 'Favorites Only' Filter")
if (sort.second == "desc") {
mangas.sortedByDescending { it.fav }
} else {
mangas.sortedBy { it.fav }
}
}
else -> {
if (sort.second == "desc") {
mangas.sortedByDescending { it.updatedDate }
@ -203,7 +250,7 @@ open class Kemono(
var hasNextPage = true
val result = ArrayList<SChapter>()
while (offset < prefMaxPost && hasNextPage) {
val request = GET("$baseUrl/$apiPath${manga.url}?o=$offset", headers)
val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers)
val page: List<KemonoPostDto> = retry(request).parseAs()
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
offset += PAGE_POST_LIMIT
@ -252,10 +299,6 @@ open class Kemono(
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = POST_PAGES_PREF
@ -284,7 +327,7 @@ open class Kemono(
getSortsList,
),
TypeFilter("Types", getTypes),
FavouritesFilter(),
FavoritesFilter(),
)
open val getTypes: List<String> = emptyList()
@ -295,7 +338,7 @@ open class Kemono(
Pair("Date Updated", "lat"),
Pair("Alphabetical Order", "tit"),
Pair("Service", "serv"),
Pair("Date Favourited", "fav"),
Pair("Date Favorited", "fav"),
)
internal open class TypeFilter(name: String, vals: List<String>) :
@ -304,17 +347,19 @@ open class Kemono(
vals.map { TriFilter(it, it.lowercase()) },
)
internal class FavouritesFilter() :
internal class FavoritesFilter() :
Filter.Group<TriFilter>(
"Favourites",
listOf(TriFilter("Favourites Only", "fav")),
"Favorites",
listOf(TriFilter("Favorites Only", "fav")),
)
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
companion object {
private const val PAGE_POST_LIMIT = 50
private const val PAGE_CREATORS_LIMIT = 50

View File

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class KemonoFavouritesDto(
class KemonoFavoritesDto(
val id: String,
val name: String,
val service: String,

View File

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

View File

@ -589,7 +589,7 @@ abstract class LibGroup(
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
private const val API_DOMAIN_TITLE = "Выбор домена API"
private const val API_DOMAIN_DEFAULT = "https://api.imglib.info"
private const val API_DOMAIN_DEFAULT = "https://api.cdnlibs.org"
private const val TOKEN_STORE = "TokenStore"
@ -652,8 +652,8 @@ abstract class LibGroup(
val domainApiPref = ListPreference(screen.context).apply {
key = API_DOMAIN_PREF
title = API_DOMAIN_TITLE
entries = arrayOf("Официальное приложение (api.imglib.info)", "Основной (api.lib.social)", "Резервный (api.mangalib.me)", "Резервный 2 (api2.mangalib.me)")
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api.lib.social", "https://api.mangalib.me", "https://api2.mangalib.me")
entries = arrayOf("Основной (api.cdnlibs.org)", "Резервный (api2.mangalib.me)", "Резервный (hapi.hentaicdn.org)", "Резервный (api.imglib.info)")
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api2.mangalib.me", "https://hapi.hentaicdn.org", "https://api.imglib.info")
summary = "%s" +
"\n\nВыбор домена API, используемого для работы приложения." +
"\n\nПо умолчанию «Официальное приложение»" +

View File

@ -1,6 +0,0 @@
font_size_title=Font size
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
font_size_message=Font size changed to %s
default_font_size=Default
disable_website_setting_title=Disable source settings
disable_website_setting_summary=Site fonts will be disabled and your device's fonts will be applied. This does not apply to downloaded or cached chapters.

View File

@ -1,6 +0,0 @@
font_size_title=Tamanho da fonte
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
font_size_message=Tamanho da fonte foi alterada para %s
default_font_size=Padrão
disable_website_setting_title=Desativar configurações do site
disable_website_setting_summary=As fontes do site serão desativadas e as fontes de seu dispositivo serão aplicadas. Isso não se aplica a capítulos baixados ou armazenados em cache.

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,345 +0,0 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O)
abstract class MachineTranslations(
override val name: String,
override val baseUrl: String,
private val language: Language,
) : ParsedHttpSource(), ConfigurableSource {
override val supportsLatest = true
private val json: Json by injectLazy()
override val lang = language.lang
protected val preferences: SharedPreferences by getPreferencesLazy()
/**
* A flag that tracks whether the settings have been changed. It is used to indicate if
* any configuration change has occurred. Once the value is accessed, it resets to `false`.
* This is useful for tracking whether a preference has been modified, and ensures that
* the change status is cleared after it has been accessed, to prevent multiple triggers.
*/
private var isSettingsChanged: Boolean = false
get() {
val current = field
field = false
return current
}
protected var fontSize: Int
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
protected var disableSourceSettings: Boolean
get() = preferences.getBoolean(DISABLE_SOURCE_SETTINGS_PREF, language.disableSourceSettings)
set(value) = preferences.edit().putBoolean(DISABLE_SOURCE_SETTINGS_PREF, value).apply()
private val intl = Intl(
language = language.lang,
baseLanguage = "en",
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
private val settings get() = language.apply {
fontSize = this@MachineTranslations.fontSize
}
open val useDefaultComposedImageInterceptor: Boolean = true
override val client: OkHttpClient get() = clientInstance!!
/**
* This ensures that the `OkHttpClient` instance is only created when required, and it is rebuilt
* when there are configuration changes to ensure that the client uses the most up-to-date settings.
*/
private var clientInstance: OkHttpClient? = null
get() {
if (field == null || isSettingsChanged) {
field = clientBuilder().build()
}
return field
}
protected open fun clientBuilder() = network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES)
.addInterceptorIf(useDefaultComposedImageInterceptor, ComposedImageInterceptor(baseUrl, settings))
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
}
// ============================== Popular ===============================
private val popularFilter = FilterList(SelectionList("", listOf(Option(value = "views", query = "sort_by"))))
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
// =============================== Latest ===============================
private val latestFilter = FilterList(SelectionList("", listOf(Option(value = "recent", query = "sort_by"))))
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
// =========================== Search ============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
if (query.isNotBlank()) {
url.addQueryParameter("query", query)
}
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected()
if (selected.value.isBlank()) {
return@forEach
}
url.addQueryParameter(selected.query, selected.value)
}
is GenreList -> {
filter.state.filter(GenreCheckBox::state).forEach { genre ->
url.addQueryParameter("genres", genre.id)
}
}
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.removePrefix(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/comics/$slug" }).map { manga ->
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = "section h2 + div > div"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun searchMangaNextPageSelector() = "a[href*=search]:contains(Next)"
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst("p:has(span:contains(Synopsis))")?.ownText()
author = document.selectFirst("p:has(span:contains(Author))")?.ownText()
genre = document.select("h2:contains(Genres) + div span").joinToString { it.text() }
thumbnail_url = document.selectFirst("img.object-cover")?.absUrl("src")
document.selectFirst("p:has(span:contains(Status))")?.ownText()?.let {
status = when (it.lowercase()) {
"ongoing" -> SManga.ONGOING
"complete" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
// ============================== Chapters ==============================
override fun chapterListSelector() = "section li"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
element.selectFirst("a")!!.let {
name = it.ownText()
setUrlWithoutDomain(it.absUrl("href"))
}
date_upload = parseChapterDate(element.selectFirst("span")?.text())
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
val pages = document.selectFirst("div#json-data")
?.ownText()?.parseAs<List<PageDto>>()
?: throw Exception("Pages not found")
return pages.mapIndexed { index, dto ->
val imageUrl = when {
dto.imageUrl.startsWith("http") -> dto.imageUrl
else -> "https://${dto.imageUrl}"
}
val fragment = json.encodeToString<List<Dialog>>(
dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
)
Page(index, imageUrl = "$imageUrl#$fragment")
}
}
override fun imageUrlParse(document: Document): String = ""
// ============================= Utilities ==============================
private fun parseChapterDate(date: String?): Long {
date ?: return 0
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
date.contains("day", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
date.contains("hour", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
date.contains("minute", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
date.contains("second", true) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
date.contains("week", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
else -> 0
}
}
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
// =============================== Filters ================================
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
SelectionList("Sort", sortByList),
Filter.Separator(),
GenreList(title = "Genres", genres = genreList),
)
return FilterList(filters)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Some libreoffice font sizes
val sizes = arrayOf(
"24", "26", "28",
"32", "36", "40",
"42", "44", "48",
"54", "60", "72",
"80", "88", "96",
)
ListPreference(screen.context).apply {
key = FONT_SIZE_PREF
title = intl["font_size_title"]
entries = sizes.map {
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
}.toTypedArray()
entryValues = sizes
summary = intl["font_size_summary"]
setOnPreferenceChange { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entries[index] as String
fontSize = selected.toInt()
Toast.makeText(
screen.context,
intl["font_size_message"].format(entry),
Toast.LENGTH_LONG,
).show()
true // It's necessary to update the user interface
}
}.also(screen::addPreference)
if (language.disableSourceSettings.not()) {
SwitchPreferenceCompat(screen.context).apply {
key = DISABLE_SOURCE_SETTINGS_PREF
title = "${intl["disable_website_setting_title"]}"
summary = intl["disable_website_setting_summary"]
setDefaultValue(false)
setOnPreferenceChange { _, newValue ->
disableSourceSettings = newValue as Boolean
true
}
}.also(screen::addPreference)
}
}
/**
* Sets an `OnPreferenceChangeListener` for the preference, and before triggering the original listener,
* marks that the configuration has changed by setting `isSettingsChanged` to `true`.
* This behavior is useful for applying runtime configurations in the HTTP client,
* ensuring that the preference change is registered before invoking the original listener.
*/
protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) {
setOnPreferenceChangeListener { preference, newValue ->
isSettingsChanged = true
onPreferenceChangeListener.onPreferenceChange(preference, newValue)
}
}
companion object {
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
const val PREFIX_SEARCH = "id:"
private const val FONT_SIZE_PREF = "fontSizePref"
private const val DISABLE_SOURCE_SETTINGS_PREF = "disableSourceSettingsPref"
private const val DEFAULT_FONT_SIZE = "24"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
}
}

View File

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
class MachineTranslationsFactoryUtils
interface Language {
val lang: String
val target: String
val origin: String
var fontSize: Int
var disableSourceSettings: Boolean
}
data class LanguageImpl(
override val lang: String,
override val target: String = lang,
override val origin: String = "en",
override var fontSize: Int = 24,
override var disableSourceSettings: Boolean = false,
) : Language

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
import eu.kanade.tachiyomi.source.model.Filter
class SelectionList(displayName: String, private val vals: List<Option>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
data class Option(val name: String = "", val value: String = "", val query: String = "")
class GenreList(title: String, genres: List<Genre>) :
Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
class Genre(val name: String, val id: String = name)
val genreList: List<Genre> = listOf(
Genre("Action"),
Genre("Adult"),
Genre("Adventure"),
Genre("Comedy"),
Genre("Drama"),
Genre("Ecchi"),
Genre("Fantasy"),
Genre("Gender Bender"),
Genre("Harem"),
Genre("Historical"),
Genre("Horror"),
Genre("Josei"),
Genre("Lolicon"),
Genre("Martial Arts"),
Genre("Mature"),
Genre("Mecha"),
Genre("Mystery"),
Genre("Psychological"),
Genre("Romance"),
Genre("School Life"),
Genre("Sci-fi"),
Genre("Seinen"),
Genre("Shoujo"),
Genre("Shoujo Ai"),
Genre("Shounen"),
Genre("Shounen Ai"),
Genre("Slice of Life"),
Genre("Smut"),
Genre("Sports"),
Genre("Supernatural"),
Genre("Tragedy"),
Genre("Yaoi"),
Genre("Yuri"),
)
val sortByList = listOf(
Option("All"),
Option("Most Views", "views"),
Option("Most Recent", "recent"),
).map { it.copy(query = "sort_by") }

View File

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

View File

@ -163,6 +163,7 @@ abstract class Madara(
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
open val popularMangaUrlSelector = "div.post-title a"
open val popularMangaUrlSelectorImg = "img"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
@ -173,7 +174,7 @@ abstract class Madara(
manga.title = it.ownText()
}
selectFirst("img")?.let {
selectFirst(popularMangaUrlSelectorImg)?.let {
manga.thumbnail_url = imageFromElement(it)
}
}
@ -701,12 +702,11 @@ abstract class Madara(
}
}
val genres = select(mangaDetailsSelectorGenre)
.map { element -> element.text().lowercase(Locale.ROOT) }
.toMutableSet()
.mapTo(ArrayList()) { element -> element.text() }
if (mangaDetailsSelectorTag.isNotEmpty()) {
select(mangaDetailsSelectorTag).forEach { element ->
if (genres.contains(element.text()).not() &&
if (
element.text().length <= 25 &&
element.text().contains("read", true).not() &&
element.text().contains(name, true).not() &&
@ -714,29 +714,19 @@ abstract class Madara(
element.text().contains(manga.title, true).not() &&
element.text().contains(altName, true).not()
) {
genres.add(element.text().lowercase(Locale.ROOT))
genres.add(element.text())
}
}
}
// add manga/manhwa/manhua thinggy to genre
document.selectFirst(seriesTypeSelector)?.ownText()?.let {
if (it.isEmpty().not() && it.notUpdating() && it != "-" && genres.contains(it).not()) {
genres.add(it.lowercase(Locale.ROOT))
if (it.isEmpty().not() && it.notUpdating() && it != "-") {
genres.add(it)
}
}
manga.genre = genres.toList().joinToString { genre ->
genre.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.ROOT,
)
} else {
it.toString()
}
}
}
manga.genre = genres.distinctBy(String::lowercase).joinToString()
// add alternative name to manga description
document.selectFirst(altNameSelector)?.ownText()?.let {
@ -788,7 +778,7 @@ abstract class Madara(
/**
* Get the best image quality available from srcset
*/
protected fun String.getSrcSetImage(): String? {
protected open fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
@ -960,11 +950,11 @@ abstract class Madara(
return when {
WordSet("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("detik", "segundo", "second", "วินาที").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("week", "semana").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
WordSet("month", "mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year", "año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق", "phút").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("detik", "segundo", "second", "วินาที", "giây").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("week", "semana", "tuần").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
WordSet("month", "mes", "tháng").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year", "año", "năm").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}

View File

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

View File

@ -155,7 +155,7 @@ abstract class MangaBox(
open val simpleQueryPath = "search/story/"
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap, div.comic-list > .list-comic-item-wrap"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/$popularUrlPath$page", headers)
@ -211,7 +211,7 @@ abstract class MangaBox(
}
}
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap, .list-comic-item-wrap .list-story-item"
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)

View File

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

View File

@ -96,7 +96,6 @@ abstract class MangaCatalog(
name = "$name1 - $name2"
}
url = element.select(".col-span-4 > a").attr("abs:href")
date_upload = System.currentTimeMillis()
}
// Pages

View File

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

View File

@ -113,12 +113,11 @@ abstract class ManhwaZ(
override fun searchMangaNextPageSelector(): String? = latestUpdatesNextPageSelector()
private val ongoingStatusList = listOf("ongoing", "đang ra")
private val completedStatusList = listOf("completed", "hoàn thành")
private val completedStatusList = listOf("completed", "hoàn thành", "Truyện Full")
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val statusText = document.selectFirst("div.summary-heading:contains($mangaDetailsStatusHeading) + div.summary-content")
?.text()
?.lowercase()
?: ""
title = document.selectFirst("div.post-title h1")!!.text()
@ -126,8 +125,8 @@ abstract class ManhwaZ(
description = document.selectFirst("div.summary__content")?.text()
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
status = when {
ongoingStatusList.contains(statusText) -> SManga.ONGOING
completedStatusList.contains(statusText) -> SManga.COMPLETED
ongoingStatusList.any { it.contains(statusText, ignoreCase = true) } -> SManga.ONGOING
completedStatusList.any { it.contains(statusText, ignoreCase = true) } -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = document.selectFirst("div.summary_image img")?.imgAttr()

View File

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

View File

@ -95,10 +95,10 @@ open class MMLook(
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.method == "GET") return popularMangaParse(response)
val entries = response.asJsoup().select(".col-auto").map { element ->
val entries = response.asJsoup().select(".item-data > div").map { element ->
SManga.create().apply {
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
title = element.selectFirst(".e-title")!!.text()
title = element.selectFirst(".e-title, .title")!!.text()
author = element.selectFirst(".tip")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}.formatUrl()

View File

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

View File

@ -154,6 +154,7 @@ abstract class ZeistManga(
protected open val mangaDetailsSelectorAuthor = "span#author"
protected open val mangaDetailsSelectorArtist = "span#artist"
protected open val mangaDetailsSelectorAltName = "header > p"
protected open val mangaDetailsSelectorStatus = "span[data-status]"
protected open val mangaDetailsSelectorInfo = ".y6x11p"
protected open val mangaDetailsSelectorInfoTitle = "strong"
protected open val mangaDetailsSelectorInfoDescription = "span.dt"
@ -175,6 +176,7 @@ abstract class ZeistManga(
.joinToString { it.text() }
author = profileManga.selectFirst(mangaDetailsSelectorAuthor)?.text()
artist = profileManga.selectFirst(mangaDetailsSelectorArtist)?.text()
status = parseStatus(profileManga.selectFirst(mangaDetailsSelectorStatus)?.text() ?: "")
val infoElement = profileManga.select(mangaDetailsSelectorInfo)
infoElement.forEach { element ->

View File

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

View File

@ -1,16 +1,19 @@
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.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.Element
import java.io.IOException
abstract class ZeroTheme(
override val name: String,
@ -20,15 +23,26 @@ abstract class ZeroTheme(
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override val client = network.cloudflareClient
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
open val imageLocation: String = "images"
open val imageLocation: String = "/images"
private val sourceLocation: String get() = "$cdnUrl/$imageLocation"
open val mangaSubString: String by lazy {
val response = client.newCall(GET(baseUrl, headers)).execute()
val script = response.asJsoup().select("script")
.map(Element::data)
.firstOrNull(MANGA_SUBSTRING_REGEX::containsMatchIn)
?: throw IOException("manga substring não foi localizado")
MANGA_SUBSTRING_REGEX.find(script)?.groups?.get(1)?.value
?: throw IOException("Não foi extrair a substring do manga")
}
open val chapterSubString: String = "chapter"
open val sourceLocation: String get() = "$cdnUrl$imageLocation"
// =========================== Popular ================================
@ -61,14 +75,30 @@ abstract class ZeroTheme(
// =========================== Details =================================
override fun getMangaUrl(manga: SManga) = "$baseUrl/$mangaSubString/${manga.url.substringAfterLast("/")}"
override fun mangaDetailsRequest(manga: SManga): Request {
checkEntry(manga.url)
return GET(getMangaUrl(manga), headers)
}
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
// =========================== Chapter =================================
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/$chapterSubString/${chapter.url.substringAfterLast("/")}"
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
// =========================== Pages ===================================
override fun pageListRequest(chapter: SChapter): Request {
checkEntry(chapter.url)
return GET(getChapterUrl(chapter), headers)
}
override fun pageListParse(response: Response): List<Page> =
response.toDto<PageDto>().toPageList(sourceLocation)
@ -76,8 +106,18 @@ abstract class ZeroTheme(
// =========================== Utilities ===============================
private fun checkEntry(url: String) {
if (listOf(mangaSubString, chapterSubString).any(url::contains)) {
throw IOException("Migre a obra para extensão $name")
}
}
inline fun <reified T> Response.toDto(): T {
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
return jsonString.parseAs<T>()
}
companion object {
val MANGA_SUBSTRING_REGEX = """"(\w+)\\/\{slug\}""".toRegex()
}
}

View File

@ -108,7 +108,7 @@ class MangaDto(
else -> SManga.UNKNOWN
}
genre = genres?.joinToString { it.name }
url = "/comic/$slug"
url = slug
}
@Serializable
@ -130,7 +130,7 @@ class ChapterDto(
name = number.toString()
chapter_number = number
date_upload = dateFormat.tryParse(createdAt)
url = "/chapter/$path"
url = path
}
companion object {

View File

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

View File

@ -43,6 +43,7 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.random.Random
open class BatoTo(
final override val lang: String,
@ -52,7 +53,17 @@ open class BatoTo(
private val preferences by getPreferencesLazy { migrateMirrorPref() }
override val name: String = "Bato.to"
override val baseUrl: String get() = mirror
override var baseUrl: String = ""
get() {
val current = field
if (current.isNotEmpty()) {
return current
}
field = getMirrorPref()
return field
}
override val id: Long = when (lang) {
"zh-Hans" -> 2818874445640189582
"zh-Hant" -> 38886079663327225
@ -69,7 +80,7 @@ open class BatoTo(
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
mirror = newValue as String
baseUrl = newValue as String
true
}
}
@ -93,20 +104,19 @@ open class BatoTo(
screen.addPreference(removeOfficialPref)
}
private var mirror = ""
get() {
val current = field
if (current.isNotEmpty()) {
return current
}
field = getMirrorPref()
return field
private fun getMirrorPref(): String {
if (System.getenv("CI") == "true") {
return (MIRROR_PREF_ENTRY_VALUES.drop(1) + DEPRECATED_MIRRORS).joinToString("#, ")
}
private fun getMirrorPref(): String {
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
?: let {
/* Semi-sticky mirror:
* - Don't randomize on boot
* - Don't randomize per language
* - Fallback for non-Android platform
*/
val seed = runCatching {
val pm = Injekt.get<Application>().packageManager
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
@ -114,7 +124,7 @@ open class BatoTo(
BuildConfig.VERSION_NAME.hashCode().toLong()
}
MIRROR_PREF_ENTRY_VALUES[1 + (seed % (MIRROR_PREF_ENTRIES.size - 1)).toInt()]
MIRROR_PREF_ENTRY_VALUES.drop(1).random(Random(seed))
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Buon Dua'
extClass = '.BuonDua'
extVersionCode = 4
extVersionCode = 6
isNsfw = true
}

View File

@ -93,7 +93,12 @@ class BuonDua() : ParsedHttpSource() {
val doc = response.asJsoup()
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1
// /xiuren-no-10051---10065-1127-photos-467c89d5b3e204eebe33ddbc54d905b1-47452?page=57
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-next").last()
?.absUrl("href")
?.takeIf { it.startsWith("http") }
?.toHttpUrl()
?.queryParameter("page")?.toInt() ?: 1
val basePageUrl = response.request.url
return (maxPage downTo 1).map { page ->
SChapter.create().apply {

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.comickfun.ComickUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="comick.io" />
<data android:host="comick.cc" />
<data android:host="comick.ink" />
<data android:host="comick.app" />
<data android:host="comick.fun" />
<data android:pathPattern="/comic/.*/..*" />
<data android:pathPattern="/comic/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,27 +0,0 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
ignored_tags_title=Ignored Tags
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description
include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres
group_tags_title=Group Tags (fork must support grouping)
group_tags_on=Will prefix tags with their type
group_tags_off=List all tags together
update_cover_title=Update Covers
update_cover_on=Keep cover updated
update_cover_off=Prefer first cover
local_title_title=Translated Title
local_title_on=if available
local_title_off=Use the default title from the site
score_position_title=Score Position in the Description
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,22 +0,0 @@
ignored_groups_title=Grupos Ignorados
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
show_alternative_titles_title=Mostrar Títulos Alternativos
show_alternative_titles_on=Adiciona títulos alternativos à descrição
show_alternative_titles_off=Não mostra títulos alternativos na descrição
include_tags_title=Incluir Tags
include_tags_on=Mais detalhadas, mas podem conter spoilers
include_tags_off=Apenas os gêneros básicos
group_tags_title=Agrupar Tags (necessário fork compatível)
group_tags_on=Prefixar tags com o respectivo tipo
group_tags_off=Listar todas as tags juntas
update_cover_title=Atualizar Capas
update_cover_on=Manter capas atualizadas
update_cover_off=Usar apenas a primeira capa
local_title_title=Título Traduzido
local_title_on=se disponível
local_title_off=Usar o título padrão do site
score_position_title=Posição da Nota na Descrição
score_position_top=Topo
score_position_middle=Meio
score_position_bottom=Final
score_position_none=Sem Nota

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,661 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Builder
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
import kotlin.math.min
abstract class Comick(
override val lang: String,
private val comickLang: String,
) : ConfigurableSource, HttpSource() {
override val name = "Comick"
override val baseUrl = "https://comick.io"
private val apiUrl = "https://api.comick.fun"
override val supportsLatest = true
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
explicitNulls = true
}
private lateinit var searchResponse: List<SearchManga>
private val intl by lazy {
Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
}
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = IGNORED_GROUPS_PREF
title = intl["ignored_groups_title"]
summary = intl["ignored_groups_summary"]
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putString(IGNORED_GROUPS_PREF, newValue.toString())
.commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = IGNORED_TAGS_PREF
title = intl["ignored_tags_title"]
summary = intl["ignored_tags_summary"]
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"]
summaryOn = intl["show_alternative_titles_on"]
summaryOff = intl["show_alternative_titles_off"]
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = INCLUDE_MU_TAGS_PREF
title = intl["include_tags_title"]
summaryOn = intl["include_tags_on"]
summaryOff = intl["include_tags_off"]
setDefaultValue(INCLUDE_MU_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(INCLUDE_MU_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = GROUP_TAGS_PREF
title = intl["group_tags_title"]
summaryOn = intl["group_tags_on"]
summaryOff = intl["group_tags_off"]
setDefaultValue(GROUP_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(GROUP_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = FIRST_COVER_PREF
title = intl["update_cover_title"]
summaryOff = intl["update_cover_off"]
summaryOn = intl["update_cover_on"]
setDefaultValue(FIRST_COVER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(FIRST_COVER_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = LOCAL_TITLE_PREF
title = intl["local_title_title"]
summaryOff = intl["local_title_off"]
summaryOn = intl["local_title_on"]
setDefaultValue(LOCAL_TITLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SCORE_POSITION_PREF
title = intl["score_position_title"]
summary = "%s"
entries = arrayOf(
intl["score_position_top"],
intl["score_position_middle"],
intl["score_position_bottom"],
intl["score_position_none"],
)
entryValues = arrayOf(SCORE_POSITION_DEFAULT, "middle", "bottom", "none")
setDefaultValue(SCORE_POSITION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit()
.putString(SCORE_POSITION_PREF, entry)
.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>
get() = getString(IGNORED_GROUPS_PREF, "")
?.lowercase()
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
?.sorted()
.orEmpty()
.toSet()
private val SharedPreferences.ignoredTags: String
get() = getString(IGNORED_TAGS_PREF, "")
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
.orEmpty()
.joinToString(",")
private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
private val SharedPreferences.groupTags: Boolean
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
private val SharedPreferences.localTitle: String
get() = if (getBoolean(
LOCAL_TITLE_PREF,
LOCAL_TITLE_DEFAULT,
)
) {
comickLang.lowercase()
} else {
"all"
}
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")}")
}
override val client = network.cloudflareClient.newBuilder()
.addNetworkInterceptor(::errorInterceptor)
.rateLimit(3, 1, TimeUnit.SECONDS)
.build()
private fun errorInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (
response.isSuccessful ||
"application/json" !in response.header("Content-Type").orEmpty()
) {
return response
}
val error = try {
response.parseAs<Error>()
} catch (_: Exception) {
null
}
error?.run {
throw Exception("$name error $statusCode: $message")
} ?: throw Exception("HTTP error ${response.code}")
}
/** Popular Manga **/
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("follow"),
),
)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<List<SearchManga>>()
return MangasPage(
result.map(SearchManga::toSManga),
hasNextPage = result.size >= LIMIT,
)
}
/** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("uploaded"),
),
)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
/** Manga Search **/
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(SLUG_SEARCH_PREFIX)) {
// url deep link
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
fetchMangaDetails(manga).map {
MangasPage(listOf(it), false)
}
} else if (query.isEmpty()) {
// regular filtering without text search
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map(::searchMangaParse)
} else {
// text search, no pagination in api
if (page == 1) {
client.newCall(querySearchRequest(query))
.asObservableSuccess()
.map(::querySearchParse)
} else {
Observable.just(paginatedSearchPage(page))
}
}
}
private fun querySearchRequest(query: String): Request {
val url = "$apiUrl/v1.0/search?limit=300&page=1&tachiyomi=true"
.toHttpUrl().newBuilder()
.addQueryParameter("q", query.trim())
.build()
return GET(url, headers)
}
private fun querySearchParse(response: Response): MangasPage {
searchResponse = response.parseAs()
return paginatedSearchPage(1)
}
private fun paginatedSearchPage(page: Int): MangasPage {
val end = min(page * LIMIT, searchResponse.size)
val entries = searchResponse.subList((page - 1) * LIMIT, end)
.map(SearchManga::toSManga)
return MangasPage(entries, end < searchResponse.size)
}
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").filter(String::isNotEmpty).forEach {
builder.addQueryParameter(
parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
filters.forEach { it ->
when (it) {
is CompletedFilter -> {
if (it.state) {
addQueryParameter("completed", "true")
}
}
is GenreFilter -> {
it.state.filter { it.isIncluded() }.forEach {
addQueryParameter("genres", it.value)
}
it.state.filter { it.isExcluded() }.forEach {
addQueryParameter("excludes", it.value)
}
}
is DemographicFilter -> {
it.state.filter { it.state }.forEach {
addQueryParameter("demographic", it.value)
}
}
is TypeFilter -> {
it.state.filter { it.state }.forEach {
addQueryParameter("country", it.value)
}
}
is SortFilter -> {
addQueryParameter("sort", it.getValue())
}
is StatusFilter -> {
if (it.state > 0) {
addQueryParameter("status", it.getValue())
}
}
is ContentRatingFilter -> {
if (it.state > 0) {
addQueryParameter("content_rating", it.getValue())
}
}
is CreatedAtFilter -> {
if (it.state > 0) {
addQueryParameter("time", it.getValue())
}
}
is MinimumFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("minimum", it.state)
}
}
is FromYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("from", it.state)
}
}
is ToYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("to", it.state)
}
}
is TagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "tags")
}
}
is ExcludedTagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "excluded-tags")
}
}
else -> {}
}
}
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$LIMIT")
addQueryParameter("page", "$page")
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
/** Manga Details **/
override fun mangaDetailsRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
val mangaUrl = manga.url.removeSuffix("#")
return GET("$apiUrl$mangaUrl?tachiyomi=true", headers)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response, manga).apply { initialized = true }
}
}
override fun mangaDetailsParse(response: Response): SManga =
mangaDetailsParse(response, SManga.create())
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
val mangaData = response.parseAs<Manga>()
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
val coversUrl =
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
val covers = client.newCall(GET(coversUrl)).execute()
.parseAs<Covers>().mdCovers.reversed()
val firstVol = covers.filter { it.vol == "1" }.ifEmpty { covers }
val originalCovers = firstVol
.filter { mangaData.comic.isoLang.orEmpty().startsWith(it.locale.orEmpty()) }
val localCovers = firstVol
.filter { comickLang.startsWith(it.locale.orEmpty()) }
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl${manga.url.removeSuffix("#")}"
}
/** Manga Chapter List **/
override fun chapterListRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
val mangaUrl = manga.url.removeSuffix("#")
val url = "$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
if (comickLang != "all") addQueryParameter("lang", comickLang)
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$CHAPTERS_LIMIT")
}.build()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapterListResponse = response.parseAs<ChapterList>()
val mangaUrl = response.request.url.toString()
.substringBefore("/chapters")
.substringAfter(apiUrl)
val currentTimestamp = System.currentTimeMillis()
return chapterListResponse.chapters
.filter {
val publishTime = try {
publishedDateFormat.parse(it.publishedAt)!!.time
} catch (_: ParseException) {
0L
}
val publishedChapter = publishTime <= currentTimestamp
val noGroupBlock = it.groups.map { g -> g.lowercase() }
.intersect(preferences.ignoredGroups)
.isEmpty()
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")
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
/** Chapter Pages **/
override fun pageListRequest(chapter: SChapter): Request {
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<PageList>()
val images = result.chapter.images.ifEmpty {
// cache busting
val url = response.request.url.newBuilder()
.addQueryParameter("_", System.currentTimeMillis().toString())
.build()
client.newCall(GET(url, headers)).execute()
.parseAs<PageList>().chapter.images
}
return images.mapIndexedNotNull { index, data ->
if (data.url == null) null else Page(index = index, imageUrl = data.url)
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups() {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit()
.putString(
IGNORED_GROUPS_PREF,
ignoredGroups
.split(",")
.map(String::trim)
.filter(String::isNotEmpty)
.joinToString("\n"),
)
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply()
}
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val IGNORED_TAGS_PREF = "IgnoredTags"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
const val INCLUDE_MU_TAGS_DEFAULT = false
private const val GROUP_TAGS_PREF = "GroupTags"
const val GROUP_TAGS_DEFAULT = false
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
private const val FIRST_COVER_PREF = "DefaultCover"
private const val FIRST_COVER_DEFAULT = true
private const val SCORE_POSITION_PREF = "ScorePosition"
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

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
// A legacy mapping of language codes to ensure that source IDs don't change
val legacyLanguageMappings = mapOf(
"pt-br" to "pt-BR", // Brazilian Portuguese
"zh-hk" to "zh-Hant", // Traditional Chinese,
"zh" to "zh-Hans", // Simplified Chinese
).withDefault { it } // country code matches language code
class ComickFactory : SourceFactory {
private val idMap = listOf(
"all" to 982606170401027267,
"en" to 2971557565147974499,
"pt-br" to 8729626158695297897,
"ru" to 5846182885417171581,
"fr" to 9126078936214680667,
"es-419" to 3182432228546767958,
"pl" to 7005108854993254607,
"tr" to 7186425300860782365,
"it" to 8807318985460553537,
"es" to 9052019484488287695,
"id" to 5506707690027487154,
"hu" to 7838940669485160901,
"vi" to 9191587139933034493,
"zh-hk" to 3140511316190656180,
"ar" to 8266599095155001097,
"de" to 7552236568334706863,
"zh" to 1071494508319622063,
"ca" to 2159382907508433047,
"bg" to 8981320463367739957,
"th" to 4246541831082737053,
"fa" to 3146252372540608964,
"uk" to 3505068018066717349,
"mn" to 2147260678391898600,
"ro" to 6676949771764486043,
"he" to 5354540502202034685,
"ms" to 4731643595200952045,
"tl" to 8549617092958820123,
"ja" to 8288710818308434509,
"hi" to 5176570178081213805,
"my" to 9199495862098963317,
"ko" to 3493720175703105662,
"cs" to 2651978322082769022,
"pt" to 4153491877797434408,
"nl" to 6104206360977276112,
"sv" to 979314012722687145,
"bn" to 3598159956413889411,
"no" to 5932005504194733317,
"lt" to 1792260331167396074,
"el" to 6190162673651111756,
"sr" to 571668187470919545,
"da" to 7137437402245830147,
).toMap()
override fun createSources(): List<Source> = idMap.keys.map {
object : Comick(legacyLanguageMappings.getValue(it), it) {
override val id: Long = idMap[it]!!
}
}
}

View File

@ -1,237 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.math.RoundingMode
@Serializable
class SearchManga(
private val hid: String,
private val title: String,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
) {
fun toSManga() = SManga.create().apply {
// appending # at end as part of migration from slug to hid
url = "/comic/$hid#"
title = this@SearchManga.title
thumbnail_url = parseCover(cover, mdCovers)
}
}
@Serializable
class Manga(
val comic: Comic,
private val artists: List<Name> = emptyList(),
private val authors: List<Name> = emptyList(),
private val genres: List<Genre> = emptyList(),
private val demographic: String? = null,
) {
fun toSManga(
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
scorePosition: String = SCORE_POSITION_DEFAULT,
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
covers: List<MDcovers>? = null,
groupTags: Boolean = GROUP_TAGS_DEFAULT,
titleLang: String,
): SManga {
val entryTitle = comic.altTitles.firstOrNull {
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
}?.title ?: comic.title
val titles = listOf(Title(title = comic.title)) + comic.altTitles
return SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = entryTitle
description = buildString {
if (scorePosition == "top") append(comic.fancyScore)
val desc = comic.desc?.beautifyDescription()
if (!desc.isNullOrEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append(desc)
}
if (scorePosition == "middle") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append("Alternative Titles:\n")
append(
titles.distinctBy { it.title }.filter { it.title != entryTitle }
.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
)
}
if (scorePosition == "bottom") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
}
status = comic.status.parseStatus(comic.translationComplete)
thumbnail_url = parseCover(
comic.cover,
covers ?: comic.mdCovers,
)
artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() }
genre = buildList {
comic.origination?.let { add(Genre("Origination", it.name)) }
demographic?.let { add(Genre("Demographic", it)) }
addAll(
comic.mdGenres.mapNotNull { it.genre }.sortedBy { it.group }
.sortedBy { it.name },
)
addAll(genres.sortedBy { it.group }.sortedBy { it.name })
if (includeMuTags) {
addAll(
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted()
.map { Genre("Category", it) },
)
}
}
.distinctBy { it.name }
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
}
}
}
@Serializable
class Comic(
val hid: String,
val title: String,
private val country: String? = null,
val slug: String? = null,
@SerialName("md_titles") val altTitles: List<Title> = emptyList(),
val desc: String? = null,
val status: Int? = 0,
@SerialName("translation_completed") val translationComplete: Boolean? = true,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
@SerialName("bayesian_rating") val score: String? = null,
@SerialName("iso639_1") val isoLang: String? = null,
) {
val origination = when (country) {
"jp" -> Name("Manga")
"kr" -> Name("Manhwa")
"cn" -> Name("Manhua")
else -> null
}
val fancyScore: String = if (score.isNullOrEmpty()) {
""
} else {
val stars = score.toBigDecimal().div(BigDecimal(2))
.setScale(0, RoundingMode.HALF_UP).toInt()
buildString {
append("".repeat(stars))
if (stars < 5) append("".repeat(5 - stars))
append(" $score")
}
}
}
@Serializable
class MdGenres(
@SerialName("md_genres") val genre: Genre? = null,
)
@Serializable
class Genre(
val group: String? = null,
val name: String? = null,
)
@Serializable
class MuComicCategories(
@SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(),
)
@Serializable
class MuCategories(
@SerialName("mu_categories") val category: Title? = null,
)
@Serializable
class Covers(
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
)
@Serializable
class MDcovers(
val b2key: String?,
val vol: String? = null,
val locale: String? = null,
)
@Serializable
class Title(
val title: String?,
val lang: String? = null,
)
@Serializable
class Name(
val name: String,
)
@Serializable
class ChapterList(
val chapters: List<Chapter>,
)
@Serializable
class Chapter(
private val hid: String,
private val lang: String = "",
private val title: String = "",
@SerialName("created_at") private val createdAt: String = "",
@SerialName("publish_at") val publishedAt: 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)
date_upload = createdAt.parseDate()
scanlator = groups.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
}
}
@Serializable
class PageList(
val chapter: ChapterPageData,
)
@Serializable
class ChapterPageData(
val images: List<Page>,
)
@Serializable
class Page(
val url: String? = null,
)
@Serializable
class Error(
val statusCode: Int,
val message: String,
)

View File

@ -1,208 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header(name = "The filter is ignored when using text search."),
GenreFilter("Genre", getGenresList),
DemographicFilter("Demographic", getDemographicList),
TypeFilter("Type", getTypeList),
SortFilter(),
StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"),
CreatedAtFilter("Created at", getCreatedAtList),
MinimumFilter("Minimum Chapters"),
Filter.Header("From Year, ex: 2010"),
FromYearFilter("From"),
Filter.Header("To Year, ex: 2021"),
ToYearFilter("To"),
Filter.Header("Separate tags with commas"),
TagFilter("Tags"),
ExcludedTagFilter("Excluded Tags"),
)
}
/** Filters **/
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>) :
Filter.Group<TriFilter>(name, genreList.map { TriFilter(it.first, it.second) })
internal class TagFilter(name: String) : TextFilter(name)
internal class ExcludedTagFilter(name: String) : TextFilter(name)
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
internal class CreatedAtFilter(name: String, createdAtList: List<Pair<String, String>>) :
SelectFilter(name, createdAtList)
internal class MinimumFilter(name: String) : TextFilter(name)
internal class FromYearFilter(name: String) : TextFilter(name)
internal class ToYearFilter(name: String) : TextFilter(name)
internal class SortFilter(defaultValue: String? = null, state: Int = 0) :
SelectFilter("Sort", getSortsList, state, defaultValue)
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
/** Generics **/
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
internal open class TextFilter(name: String) : Filter.Text(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0, defaultValue: String? = null) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) {
fun getValue() = vals[state].second
}
/** Filters Data **/
private val getGenresList: List<Pair<String, String>> = listOf(
Pair("4-Koma", "4-koma"),
Pair("Action", "action"),
Pair("Adaptation", "adaptation"),
Pair("Adult", "adult"),
Pair("Adventure", "adventure"),
Pair("Aliens", "aliens"),
Pair("Animals", "animals"),
Pair("Anthology", "anthology"),
Pair("Award Winning", "award-winning"),
Pair("Comedy", "comedy"),
Pair("Cooking", "cooking"),
Pair("Crime", "crime"),
Pair("Crossdressing", "crossdressing"),
Pair("Delinquents", "delinquents"),
Pair("Demons", "demons"),
Pair("Doujinshi", "doujinshi"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fan Colored", "fan-colored"),
Pair("Fantasy", "fantasy"),
Pair("Full Color", "full-color"),
Pair("Gender Bender", "gender-bender"),
Pair("Genderswap", "genderswap"),
Pair("Ghosts", "ghosts"),
Pair("Gore", "gore"),
Pair("Gyaru", "gyaru"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Incest", "incest"),
Pair("Isekai", "isekai"),
Pair("Loli", "loli"),
Pair("Long Strip", "long-strip"),
Pair("Mafia", "mafia"),
Pair("Magic", "magic"),
Pair("Magical Girls", "magical-girls"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Medical", "medical"),
Pair("Military", "military"),
Pair("Monster Girls", "monster-girls"),
Pair("Monsters", "monsters"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Ninja", "ninja"),
Pair("Office Workers", "office-workers"),
Pair("Official Colored", "official-colored"),
Pair("Oneshot", "oneshot"),
Pair("Philosophical", "philosophical"),
Pair("Police", "police"),
Pair("Post-Apocalyptic", "post-apocalyptic"),
Pair("Psychological", "psychological"),
Pair("Reincarnation", "reincarnation"),
Pair("Reverse Harem", "reverse-harem"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School Life", "school-life"),
Pair("Sci-Fi", "sci-fi"),
Pair("Sexual Violence", "sexual-violence"),
Pair("Shota", "shota"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Smut", "smut"),
Pair("Sports", "sports"),
Pair("Superhero", "superhero"),
Pair("Supernatural", "supernatural"),
Pair("Survival", "survival"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("Traditional Games", "traditional-games"),
Pair("Tragedy", "tragedy"),
Pair("User Created", "user-created"),
Pair("Vampires", "vampires"),
Pair("Video Games", "video-games"),
Pair("Villainess", "villainess"),
Pair("Virtual Reality", "virtual-reality"),
Pair("Web Comic", "web-comic"),
Pair("Wuxia", "wuxia"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombies", "zombies"),
)
private val getDemographicList: List<Pair<String, String>> = listOf(
Pair("Shounen", "1"),
Pair("Shoujo", "2"),
Pair("Seinen", "3"),
Pair("Josei", "4"),
Pair("None", "5"),
)
private val getTypeList: List<Pair<String, String>> = listOf(
Pair("Manga", "jp"),
Pair("Manhwa", "kr"),
Pair("Manhua", "cn"),
Pair("Others", "others"),
)
private val getCreatedAtList: List<Pair<String, String>> = listOf(
Pair("", ""),
Pair("3 days", "3"),
Pair("7 days", "7"),
Pair("30 days", "30"),
Pair("3 months", "90"),
Pair("6 months", "180"),
Pair("1 year", "365"),
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Most popular", "follow"),
Pair("Most follows", "user_follow_count"),
Pair("Most views", "view"),
Pair("High rating", "rating"),
Pair("Last updated", "uploaded"),
Pair("Newest", "created_at"),
)
private val getStatusList: List<Pair<String, String>> = listOf(
Pair("All", "0"),
Pair("Ongoing", "1"),
Pair("Completed", "2"),
Pair("Cancelled", "3"),
Pair("Hiatus", "4"),
)
private val getContentRatingList: List<Pair<String, String>> = listOf(
Pair("All", ""),
Pair("Safe", "safe"),
Pair("Suggestive", "suggestive"),
Pair("Erotica", "erotica"),
)

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.parser.Parser
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
private val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
private val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
private val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
internal fun String.beautifyDescription(): String {
return Parser.unescapeEntities(this, false)
.substringBefore("---")
.replace(markdownLinksRegex, "")
.replace(markdownItalicBoldRegex, "")
.replace(markdownItalicRegex, "")
.trim()
}
internal fun Int?.parseStatus(translationComplete: Boolean?): Int {
return when (this) {
1 -> SManga.ONGOING
2 -> {
if (translationComplete == true) {
SManga.COMPLETED
} else {
SManga.PUBLISHING_FINISHED
}
}
3 -> SManga.CANCELLED
4 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>): String? {
val b2key = mdCovers.firstOrNull()?.b2key
?: return thumbnailUrl
val vol = mdCovers.firstOrNull()?.vol.orEmpty()
return thumbnailUrl?.replaceAfterLast("/", "$b2key#$vol")
}
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
return buildString {
if (vol.isNotEmpty()) {
if (chap.isEmpty()) append("Volume $vol") else append("Vol. $vol")
}
if (chap.isNotEmpty()) {
if (vol.isEmpty()) append("Chapter $chap") else append(", Ch. $chap")
}
if (title.isNotEmpty()) {
if (chap.isEmpty()) append(title) else append(": $title")
}
}
}
internal fun String.parseDate(): Long {
return runCatching { dateFormat.parse(this)?.time }
.getOrNull() ?: 0L
}

View File

@ -2,7 +2,7 @@ ext {
extName = 'Coomer'
extClass = '.Coomer'
themePkg = 'kemono'
baseUrl = 'https://coomer.su'
baseUrl = 'https://coomer.st'
overrideVersionCode = 0
isNsfw = true
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.coomer
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
class Coomer : Kemono("Coomer", "https://coomer.su", "all") {
class Coomer : Kemono("Coomer", "https://coomer.st", "all") {
override val getTypes = listOf(
"OnlyFans",
"Fansly",

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

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

View File

@ -1,67 +0,0 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.SourceFactory
import keiyoushi.utils.getPreferencesLazy
class GalaxyFactory : SourceFactory {
class GalaxyWebtoon : Galaxy("Galaxy Webtoon", "https://galaxyaction.net", "en") {
override val id = 2602904659965278831
}
class GalaxyManga :
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
ConfigurableSource {
override val id = 2729515745226258240
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by getPreferencesLazy()
companion object {
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(super.baseUrl)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: ${super.baseUrl}"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(baseUrlPref)
}
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
init {
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
if (prefDefaultBaseUrl != super.baseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, super.baseUrl)
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
.apply()
}
}
}
}
override fun createSources() = listOf(
GalaxyWebtoon(),
GalaxyManga(),
)
}

View File

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

View File

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

View File

@ -209,7 +209,7 @@ class HentaiCosplay : HttpSource() {
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("amp-img[src*=upload]")
return document.select("amp-img[src*=upload]:not(.related-thumbnail)")
.mapIndexed { index, element ->
Page(
index = index,

View File

@ -1,7 +1,7 @@
ext {
extName = 'izneo (webtoons)'
extClass = '.IzneoFactory'
extVersionCode = 7
extVersionCode = 8
isNsfw = false
}

View File

@ -4,14 +4,14 @@ import android.util.Base64
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody
import okio.buffer
import okio.cipherSource
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object ImageInterceptor : Interceptor {
private val mediaType = "image/jpeg".toMediaType()
private inline val AES: Cipher
get() = Cipher.getInstance("AES/CBC/PKCS7Padding")
@ -31,7 +31,7 @@ object ImageInterceptor : Interceptor {
private fun Response.decode(key: ByteArray, iv: ByteArray) = AES.let {
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
newBuilder().body(it.doFinal(body.bytes()).toResponseBody(mediaType)).build()
newBuilder().body(body.source().cipherSource(it).buffer().asResponseBody("image/jpeg".toMediaType())).build()
}
private fun String.atob() = Base64.decode(this, Base64.URL_SAFE)

View File

@ -2,8 +2,8 @@ ext {
extName = 'Kemono'
extClass = '.Kemono'
themePkg = 'kemono'
baseUrl = 'https://kemono.su'
overrideVersionCode = 0
baseUrl = 'https://kemono.cr'
overrideVersionCode = 1
isNsfw = true
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.kemono
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
class Kemono : Kemono("Kemono", "https://kemono.su", "all") {
class Kemono : Kemono("Kemono", "https://kemono.cr", "all") {
override val getTypes = listOf(
"Patreon",
"Pixiv Fanbox",

View File

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

View File

@ -76,6 +76,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
private val password by lazy { preferences.getString(PREF_PASSWORD, "")!! }
private val apiKey by lazy { preferences.getString(PREF_API_KEY, "")!! }
private val defaultLibraries
get() = preferences.getStringSet(PREF_DEFAULT_LIBRARIES, emptySet())!!
@ -83,12 +85,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
override fun headersBuilder() = super.headersBuilder()
.set("User-Agent", "TachiyomiKomga/${AppInfo.getVersionName()}")
.also { builder ->
if (apiKey.isNotBlank()) {
builder.set("X-API-Key", apiKey)
}
}
override val client: OkHttpClient =
network.cloudflareClient.newBuilder()
.authenticator { _, response ->
if (response.request.header("Authorization") != null) {
null // Give up, we've already failed to authenticate.
if (apiKey.isNotBlank() || response.request.header("Authorization") != null) {
null // Give up if API key is set or we've already failed to authenticate.
} else {
response.request.newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
@ -377,21 +384,33 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
key = PREF_ADDRESS,
restartRequired = true,
)
// API key preference (takes precedence over username/password)
screen.addEditTextPreference(
title = "Username",
title = "API key",
default = "",
summary = username.ifBlank { "The user account email" },
key = PREF_USERNAME,
restartRequired = true,
)
screen.addEditTextPreference(
title = "Password",
default = "",
summary = if (password.isBlank()) "The user account password" else "*".repeat(password.length),
summary = if (apiKey.isBlank()) "Optional: Use an API key for authentication" else "*".repeat(apiKey.length),
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
key = PREF_PASSWORD,
key = PREF_API_KEY,
restartRequired = true,
)
// Only show username/password if API key is not set
if (apiKey.isBlank()) {
screen.addEditTextPreference(
title = "Username",
default = "",
summary = username.ifBlank { "The user account email" },
key = PREF_USERNAME,
restartRequired = true,
)
screen.addEditTextPreference(
title = "Password",
default = "",
summary = if (password.isBlank()) "The user account password" else "*".repeat(password.length),
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
key = PREF_PASSWORD,
restartRequired = true,
)
}
MultiSelectListPreference(screen.context).apply {
key = PREF_DEFAULT_LIBRARIES
@ -529,6 +548,7 @@ private const val PREF_DISPLAY_NAME = "Source display name"
private const val PREF_ADDRESS = "Address"
private const val PREF_USERNAME = "Username"
private const val PREF_PASSWORD = "Password"
private const val PREF_API_KEY = "API key"
private const val PREF_DEFAULT_LIBRARIES = "Default libraries"
private const val PREF_CHAPTER_NAME_TEMPLATE = "Chapter name template"
private const val PREF_CHAPTER_NAME_TEMPLATE_DEFAULT = "{number} - {title} ({size})"

View File

@ -1,7 +1,7 @@
ext {
extName = 'Luscious'
extClass = '.LusciousFactory'
extVersionCode = 20
extVersionCode = 22
isNsfw = true
}

View File

@ -31,11 +31,11 @@ import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
@ -63,8 +63,8 @@ abstract class Luscious(
private val rewriteOctetStream: Interceptor = Interceptor { chain ->
val originalResponse: Response = chain.proceed(chain.request())
if (originalResponse.headers("Content-Type").contains("application/octet-stream") && originalResponse.request.url.toString().contains(".webp")) {
val orgBody = originalResponse.body.bytes()
val newBody = orgBody.toResponseBody("image/webp".toMediaTypeOrNull())
val orgBody = originalResponse.body.source()
val newBody = orgBody.asResponseBody("image/webp".toMediaType())
originalResponse.newBuilder()
.body(newBody)
.build()
@ -845,7 +845,7 @@ abstract class Luscious(
private const val MIRROR_PREF_KEY = "MIRROR"
private const val MIRROR_PREF_TITLE = "Mirror"
private val MIRROR_PREF_ENTRIES = arrayOf("Guest", "API", "Members")
private val MIRROR_PREF_ENTRY_VALUES = arrayOf("https://www.luscious.net", "https://api.luscious.net", "https://members.luscious.net")
private val MIRROR_PREF_ENTRY_VALUES = arrayOf("https://www.luscious.net", "https://apicdn.luscious.net", "https://members.luscious.net")
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaDex'
extClass = '.MangaDexFactory'
extVersionCode = 203
extVersionCode = 204
isNsfw = true
}

View File

@ -30,7 +30,6 @@ object MDConstants {
const val apiMangaUrl = "$apiUrl/manga"
const val apiChapterUrl = "$apiUrl/chapter"
const val apiListUrl = "$apiUrl/list"
const val atHomePostUrl = "https://api.mangadex.network/report"
val whitespaceRegex = "\\s".toRegex()
val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds

View File

@ -73,7 +73,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
override val client = network.cloudflareClient.newBuilder()
.rateLimit(3)
.addInterceptor(MdAtHomeReportInterceptor(network.cloudflareClient, headers))
.build()
// Popular manga section

View File

@ -1,109 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangadex
import android.util.Log
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ImageReportDto
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
/**
* Interceptor to post to MD@Home for MangaDex Stats
*/
class MdAtHomeReportInterceptor(
private val client: OkHttpClient,
private val headers: Headers,
) : Interceptor {
private val json: Json by injectLazy()
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(chain.request())
val url = originalRequest.url.toString()
if (!url.contains(MD_AT_HOME_URL_REGEX)) {
return response
}
Log.e("MangaDex", "Connecting to MD@Home node at $url")
val reportRequest = mdAtHomeReportRequest(response)
// Execute the report endpoint network call asynchronously to avoid blocking
// the reader from showing the image once it's fully loaded if the report call
// gets stuck, as it tend to happens sometimes.
client.newCall(reportRequest).enqueue(REPORT_CALLBACK)
if (response.isSuccessful) {
return response
}
response.close()
Log.e("MangaDex", "Error connecting to MD@Home node, fallback to uploads server")
val imagePath = originalRequest.url.pathSegments
.dropWhile { it != "data" && it != "data-saver" }
.joinToString("/")
val fallbackUrl = MDConstants.cdnUrl.toHttpUrl().newBuilder()
.addPathSegments(imagePath)
.build()
val fallbackRequest = originalRequest.newBuilder()
.url(fallbackUrl)
.headers(headers)
.build()
return chain.proceed(fallbackRequest)
}
private fun mdAtHomeReportRequest(response: Response): Request {
val result = ImageReportDto(
url = response.request.url.toString(),
success = response.isSuccessful,
bytes = response.peekBody(Long.MAX_VALUE).bytes().size,
cached = response.headers["X-Cache"] == "HIT",
duration = response.receivedResponseAtMillis - response.sentRequestAtMillis,
)
val payload = json.encodeToString(result)
return POST(
url = MDConstants.atHomePostUrl,
headers = headers,
body = payload.toRequestBody(JSON_MEDIA_TYPE),
)
}
companion object {
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
private val MD_AT_HOME_URL_REGEX =
"""^https://[\w\d]+\.[\w\d]+\.mangadex(\b-test\b)?\.network.*${'$'}""".toRegex()
private val REPORT_CALLBACK = object : Callback {
override fun onFailure(call: Call, e: okio.IOException) {
Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
Log.e("MangaDex", "Error trying to POST report to MD@Home: HTTP error ${response.code}")
}
response.close()
}
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'MANGA Plus by SHUEISHA'
extClass = '.MangaPlusFactory'
extVersionCode = 54
extVersionCode = 55
}
apply from: "$rootDir/common.gradle"

View File

@ -211,6 +211,7 @@ class Label(val label: LabelCode? = LabelCode.WEEKLY_SHOUNEN_JUMP) {
LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators"
LabelCode.SAIKYOU_JUMP -> "Saikyou Jump"
LabelCode.ULTRA_JUMP -> "Ultra Jump"
LabelCode.DX -> "Dash X Comic"
else -> null
}
}
@ -250,6 +251,9 @@ enum class LabelCode {
@SerialName("UJ")
ULTRA_JUMP,
@SerialName("DX")
DX,
}
@Serializable

View File

@ -3,7 +3,7 @@
<application>
<activity
android:name=".zh.dmzj.DmzjUrlActivity"
android:name=".all.mangapluscreators.MPCUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -14,28 +14,29 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="m.dmzj.com"
android:pathPattern="/info/..*"
android:host="mangaplus-creators.jp"
android:pathAdvancedPattern="/episodes/.+"
android:scheme="https" />
<data
android:host="www.dmzj.com"
android:pathPattern="/info/..*"
android:host="mangaplus-creators.jp"
android:pathPattern="/titles/..*"
android:scheme="https" />
<data
android:host="manhua.dmzj.com"
android:pathPattern="/..*"
android:host="mangaplus-creators.jp"
android:pathAdvancedPattern="/authors/.+"
android:scheme="https" />
<data
android:host="medibang.com"
android:pathAdvancedPattern="/mpc/episodes/.+"
android:scheme="https" />
<data
android:host="m.muwai.com"
android:pathPattern="/info/..*"
android:host="medibang.com"
android:pathAdvancedPattern="/mpc/titles/..+"
android:scheme="https" />
<data
android:host="www.muwai.com"
android:pathPattern="/info/..*"
android:scheme="https" />
<data
android:host="manhua.muwai.com"
android:pathPattern="/..*"
android:host="medibang.com"
android:pathAdvancedPattern="/mpc/authors/[0-9]+"
android:scheme="https" />
</intent-filter>
</activity>

View File

@ -1,7 +1,7 @@
ext {
extName = 'MANGA Plus Creators by SHUEISHA'
extClass = '.MangaPlusCreatorsFactory'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.extension.all.mangapluscreators
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 MPCUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
// {medibang.com/mpc,mangaplus-creators.jp}/{episodes,titles,authors}
// TODO: val pathIndex = if (intent?.data?.host?.startsWith("medibang") == true) 1 else 0
val host = intent?.data?.host ?: ""
val pathIndex = with(host) {
when {
equals("medibang.com") -> 1
else -> 0
}
}
val idIndex = pathIndex + 1
val query = when {
pathSegments[pathIndex].equals("episodes") -> {
MangaPlusCreators.PREFIX_EPISODE_ID_SEARCH + pathSegments[idIndex]
}
pathSegments[pathIndex].equals("authors") -> {
MangaPlusCreators.PREFIX_AUTHOR_ID_SEARCH + pathSegments[idIndex]
}
pathSegments[pathIndex].equals("titles") -> {
MangaPlusCreators.PREFIX_TITLE_ID_SEARCH + pathSegments[idIndex]
}
else -> null // TODO: is this required?
}
if (query != null) {
// TODO: val mainIntent = Intent().setAction("eu.kanade.tachiyomi.SEARCH").apply {
val mainIntent = Intent().apply {
setAction("eu.kanade.tachiyomi.SEARCH")
putExtra("query", query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("MPCUrlActivity", e.toString())
}
} else {
Log.e("MPCUrlActivity", "Missing alphanumeric ID from the URL")
}
} else {
Log.e("MPCUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.extension.all.mangapluscreators
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -8,102 +10,199 @@ 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 keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MangaPlusCreators(override val lang: String) : HttpSource() {
override val name = "MANGA Plus Creators by SHUEISHA"
override val baseUrl = "https://medibang.com/mpc"
override val baseUrl = "https://mangaplus-creators.jp"
private val apiUrl = "$baseUrl/api"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl.substringBeforeLast("/"))
.add("Referer", baseUrl)
.add("User-Agent", USER_AGENT)
private val json: Json by injectLazy()
// POPULAR Section
override fun popularMangaRequest(page: Int): Request {
val newHeaders = headersBuilder()
.set("Referer", "$baseUrl/titles/popular/?p=m")
.add("X-Requested-With", "XMLHttpRequest")
.build()
val apiUrl = "$API_URL/titles/popular/list".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
.addQueryParameter("l", lang)
.addQueryParameter("p", "m")
.addQueryParameter("isWebview", "false")
.addQueryParameter("_", System.currentTimeMillis().toString())
.toString()
return GET(apiUrl, newHeaders)
val popularUrl = "$baseUrl/titles/popular/?p=m&l=$lang".toHttpUrl()
return GET(popularUrl, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.asMpcResponse()
override fun popularMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
response,
"div.item-recent",
)
checkNotNull(result.titles) { EMPTY_RESPONSE_ERROR }
private fun parseMangasPageFromElement(response: Response, selector: String): MangasPage {
val result = response.asJsoup()
val titles = result.titles.titleList.orEmpty().map(MpcTitle::toSManga)
val mangas = result.select(selector).map { element ->
popularElementToSManga(element)
}
return MangasPage(titles, result.titles.pagination?.hasNextPage ?: false)
return MangasPage(mangas, false)
}
private fun popularElementToSManga(element: Element): SManga {
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
return SManga.create().apply {
title = element.selectFirst(".title-area .title")!!.text()
thumbnail_url = titleThumbnailUrl
setUrlWithoutDomain("/titles/$titleContentId")
}
}
// LATEST Section
override fun latestUpdatesRequest(page: Int): Request {
val newHeaders = headersBuilder()
.set("Referer", "$baseUrl/titles/recent/?t=episode")
.add("X-Requested-With", "XMLHttpRequest")
val apiUrl = "$apiUrl/titles/recent/".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("l", lang)
.addQueryParameter("t", "episode")
.build()
val apiUrl = "$API_URL/titles/recent/list".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
.addQueryParameter("l", lang)
.addQueryParameter("c", "episode")
.addQueryParameter("isWebview", "false")
.addQueryParameter("_", System.currentTimeMillis().toString())
.toString()
return GET(apiUrl, newHeaders)
return GET(apiUrl, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<MpcResponse>()
val titles = result.titles.orEmpty().map { title -> title.toSManga() }
// TODO: handle last page of latest
return MangasPage(titles, result.status != "error")
}
private fun MpcTitle.toSManga(): SManga {
val mTitle = this.title
val mAuthor = this.author.name // TODO: maybe not required
return SManga.create().apply {
title = mTitle
thumbnail_url = thumbnail
setUrlWithoutDomain("/titles/${latestEpisode.titleConnectId}")
author = mAuthor
}
}
// SEARCH Section
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
// TODO: HTTPSource::fetchSearchManga is deprecated? super.getSearchManga
if (query.startsWith(PREFIX_TITLE_ID_SEARCH)) {
val titleContentId = query.removePrefix(PREFIX_TITLE_ID_SEARCH)
val titleUrl = "$baseUrl/titles/$titleContentId"
return client.newCall(GET(titleUrl, headers))
.asObservableSuccess()
.map { response ->
val result = response.asJsoup()
val bookBox = result.selectFirst(".book-box")!!
val title = SManga.create().apply {
title = bookBox.selectFirst("div.title")!!.text()
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
setUrlWithoutDomain(titleUrl)
}
MangasPage(listOf(title), false)
}
}
if (query.startsWith(PREFIX_EPISODE_ID_SEARCH)) {
val episodeId = query.removePrefix(PREFIX_EPISODE_ID_SEARCH)
return client.newCall(GET("$baseUrl/episodes/$episodeId", headers))
.asObservableSuccess().map { response ->
val result = response.asJsoup()
val readerElement = result.selectFirst("div[react=viewer]")!!
val dataTitle = readerElement.attr("data-title")
val dataTitleResult = dataTitle.parseAs<MpcReaderDataTitle>()
val episodeAsSManga = dataTitleResult.toSManga()
MangasPage(listOf(episodeAsSManga), false)
}
}
if (query.startsWith(PREFIX_AUTHOR_ID_SEARCH)) {
val authorId = query.removePrefix(PREFIX_AUTHOR_ID_SEARCH)
return client.newCall(GET("$baseUrl/authors/$authorId", headers))
.asObservableSuccess()
.map { response ->
val result = response.asJsoup()
val elements = result.select("#works .manga-list li .md\\:block")
val smangas = elements.map { element ->
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
SManga.create().apply {
title = element.selectFirst("p.text-white")!!.text().toString()
thumbnail_url = titleThumbnailUrl
setUrlWithoutDomain("/titles/$titleContentId")
}
}
MangasPage(smangas, false)
}
}
if (query.isNotBlank()) {
return super.fetchSearchManga(page, query, filters)
}
// nothing to search, filters active -> browsing /genres instead
// TODO: check if there's a better way (filters is independent of search but part of it)
val genreUrl = baseUrl.toHttpUrl().newBuilder()
.apply {
addPathSegment("genres")
addQueryParameter("l", lang)
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
if (filter.selected.isNotEmpty()) {
addQueryParameter("s", filter.selected)
}
}
is GenreFilter -> addPathSegment(filter.selected)
else -> { /* Nothing else is supported for now */ }
}
}
}.build()
return client.newCall(GET(genreUrl, headers))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
}
private fun MpcReaderDataTitle.toSManga(): SManga {
val mTitle = title
return SManga.create().apply {
title = mTitle
thumbnail_url = thumbnail
setUrlWithoutDomain("/titles/$contentsId")
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val refererUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
// TODO: maybe this needn't be a new builder and just similar to `popularUrl` above?
val searchUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.toString()
val newHeaders = headersBuilder()
.set("Referer", refererUrl)
.add("X-Requested-With", "XMLHttpRequest")
.addQueryParameter("s", "date")
.addQueryParameter("lang", lang)
.build()
val apiUrl = "$API_URL/search/titles".toHttpUrl().newBuilder()
.addQueryParameter("keyword", query)
.addQueryParameter("page", page.toString())
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
.addQueryParameter("sort", "newly")
.addQueryParameter("lang", lang)
.addQueryParameter("_", System.currentTimeMillis().toString())
.toString()
return GET(apiUrl, newHeaders)
return GET(searchUrl, headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun searchMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
response,
"div.item-search",
)
// MANGA Section
override fun mangaDetailsParse(response: Response): SManga {
val result = response.asJsoup()
val bookBox = result.selectFirst(".book-box")!!
@ -119,62 +218,82 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
else -> SManga.UNKNOWN
}
genre = bookBox.select("div.genre-area div.tag-genre")
.joinToString { it.text() }
.joinToString(", ") { it.text() }
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
}
}
// CHAPTER Section
override fun chapterListRequest(manga: SManga): Request {
val titleId = manga.url.substringAfterLast("/")
val titleContentId = (baseUrl + manga.url).toHttpUrl().pathSegments[1]
return chapterListPageRequest(1, titleContentId)
}
val newHeaders = headersBuilder()
.set("Referer", baseUrl + manga.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val apiUrl = "$API_URL/titles/$titleId/episodes/".toHttpUrl().newBuilder()
.addQueryParameter("page", "1")
.addQueryParameter("pageSize", CHAPTER_PAGE_SIZE)
.addQueryParameter("_", System.currentTimeMillis().toString())
.toString()
return GET(apiUrl, newHeaders)
private fun chapterListPageRequest(page: Int, titleContentId: String): Request {
return GET("$baseUrl/titles/$titleContentId/?page=$page", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.asMpcResponse()
val chapterListResponse = chapterListPageParse(response)
val chapterListResult = chapterListResponse.chapters.toMutableList()
checkNotNull(result.episodes) { EMPTY_RESPONSE_ERROR }
var hasNextPage = chapterListResponse.hasNextPage
val titleContentId = response.request.url.pathSegments[1]
var page = 1
while (hasNextPage) {
page += 1
val nextPageRequest = chapterListPageRequest(page, titleContentId)
val nextPageResponse = client.newCall(nextPageRequest).execute()
val nextPageResult = chapterListPageParse(nextPageResponse)
if (nextPageResult.chapters.isEmpty()) {
break
}
chapterListResult.addAll(nextPageResult.chapters)
hasNextPage = nextPageResult.hasNextPage
}
return result.episodes.episodeList.orEmpty()
.sortedByDescending(MpcEpisode::numbering)
.map(MpcEpisode::toSChapter)
return chapterListResult.asReversed()
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast("/")
val newHeaders = headersBuilder()
.set("Referer", baseUrl + chapter.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val apiUrl = "$API_URL/episodes/pageList/$chapterId/".toHttpUrl().newBuilder()
.addQueryParameter("_", System.currentTimeMillis().toString())
.toString()
return GET(apiUrl, newHeaders)
private fun chapterListPageParse(response: Response): ChaptersPage {
val result = response.asJsoup()
val chapters = result.select(".mod-item-series").map {
element ->
chapterElementToSChapter(element)
}
val hasResult = result.select(".mod-pagination .next").isNotEmpty()
return ChaptersPage(
chapters,
hasResult,
)
}
private fun chapterElementToSChapter(element: Element): SChapter {
val episode = element.attr("href").substringAfterLast("/")
val latestUpdatedDate = element.selectFirst(".first-update")!!.text()
val chapterNumberElement = element.selectFirst(".number")!!.text()
val chapterNumber = chapterNumberElement.substringAfter("#").toFloatOrNull()
return SChapter.create().apply {
setUrlWithoutDomain("/episodes/$episode")
date_upload = CHAPTER_DATE_FORMAT.tryParse(latestUpdatedDate)
name = chapterNumberElement
chapter_number = if (chapterNumberElement == "One-shot") {
0F
} else {
chapterNumber ?: -1F
}
}
}
// PAGES & IMAGES Section
override fun pageListParse(response: Response): List<Page> {
val result = response.asMpcResponse()
checkNotNull(result.pageList) { EMPTY_RESPONSE_ERROR }
val referer = response.request.header("Referer")!!
return result.pageList.mapIndexed { i, page ->
Page(i, referer, page.publicBgImage)
val result = response.asJsoup()
val readerElement = result.selectFirst("div[react=viewer]")!!
val dataPages = readerElement.attr("data-pages")
val refererUrl = response.request.url.toString()
return dataPages.parseAs<MpcReaderDataPages>().pc.map {
page ->
Page(page.pageNo, refererUrl, page.imageUrl)
}
}
@ -191,18 +310,66 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
return GET(page.imageUrl!!, newHeaders)
}
private fun Response.asMpcResponse(): MpcResponse = use {
json.decodeFromString(body.string())
}
companion object {
private const val API_URL = "https://medibang.com/api/mpc"
private val CHAPTER_DATE_FORMAT by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
private const val POPULAR_PAGE_SIZE = "30"
private const val CHAPTER_PAGE_SIZE = "200"
private const val EMPTY_RESPONSE_ERROR = "Empty response from the API. Try again later."
const val PREFIX_TITLE_ID_SEARCH = "title:"
const val PREFIX_EPISODE_ID_SEARCH = "episode:"
const val PREFIX_AUTHOR_ID_SEARCH = "author:"
}
// FILTERS Section
override fun getFilterList() = FilterList(
Filter.Separator(),
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
SortFilter(),
GenreFilter(),
Filter.Separator(),
)
private class SortFilter() : SelectFilter(
"Sort",
listOf(
SelectFilterOption("Popularity", ""),
SelectFilterOption("Date", "latest_desc"),
SelectFilterOption("Likes", "like_desc"),
),
0,
)
private class GenreFilter() : SelectFilter(
"Genres",
listOf(
SelectFilterOption("Fantasy", "fantasy"),
SelectFilterOption("Action", "action"),
SelectFilterOption("Romance", "romance"),
SelectFilterOption("Horror", "horror"),
SelectFilterOption("Slice of Life", "slice_of_life"),
SelectFilterOption("Comedy", "comedy"),
SelectFilterOption("Sports", "sports"),
SelectFilterOption("Sci-Fi", "sf"),
SelectFilterOption("Mystery", "mystery"),
SelectFilterOption("Others", "others"),
),
0,
)
private abstract class SelectFilter(
name: String,
private val options: List<SelectFilterOption>,
default: Int = 0,
) : Filter.Select<String>(
name,
options.map { it.name }.toTypedArray(),
default,
) {
val selected: String
get() = options[state].value
}
private class SelectFilterOption(val name: String, val value: String)
}

View File

@ -1,68 +1,51 @@
package eu.kanade.tachiyomi.extension.all.mangapluscreators
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MpcResponse(
@SerialName("mpcEpisodesDto") val episodes: MpcEpisodesDto? = null,
@SerialName("mpcTitlesDto") val titles: MpcTitlesDto? = null,
val pageList: List<MpcPage>? = emptyList(),
class MpcResponse(
val status: String,
val titles: List<MpcTitle>? = null,
)
@Serializable
data class MpcEpisodesDto(
val pagination: MpcPagination? = null,
val episodeList: List<MpcEpisode>? = emptyList(),
)
@Serializable
data class MpcTitlesDto(
val pagination: MpcPagination? = null,
val titleList: List<MpcTitle>? = emptyList(),
)
@Serializable
data class MpcPagination(
val page: Int,
val maxPage: Int,
) {
val hasNextPage: Boolean
get() = page < maxPage
}
@Serializable
data class MpcTitle(
@SerialName("titleId") val id: String,
class MpcTitle(
val title: String,
val thumbnailUrl: String,
) {
fun toSManga(): SManga = SManga.create().apply {
title = this@MpcTitle.title
thumbnail_url = thumbnailUrl
url = "/titles/$id"
}
}
val thumbnail: String,
@SerialName("is_one_shot") val isOneShot: Boolean,
val author: MpcAuthorDto,
@SerialName("latest_episode") val latestEpisode: MpcLatestEpisode,
)
@Serializable
data class MpcEpisode(
@SerialName("episodeId") val id: String,
@SerialName("episodeTitle") val title: String,
val numbering: Int,
val oneshot: Boolean = false,
val publishDate: Long,
) {
fun toSChapter(): SChapter = SChapter.create().apply {
name = if (oneshot) "One-shot" else title
date_upload = publishDate
url = "/episodes/$id"
}
}
class MpcAuthorDto(
val name: String,
)
@Serializable
data class MpcPage(val publicBgImage: String)
class MpcLatestEpisode(
@SerialName("title_connect_id") val titleConnectId: String,
)
@Serializable
class MpcReaderDataPages(
val pc: List<MpcReaderPage>,
)
@Serializable
class MpcReaderPage(
@SerialName("page_no") val pageNo: Int,
@SerialName("image_url") val imageUrl: String,
)
@Serializable
class MpcReaderDataTitle(
val title: String,
val thumbnail: String,
@SerialName("is_oneshot") val isOneShot: Boolean,
@SerialName("contents_id") val contentsId: String,
)
class ChaptersPage(val chapters: List<SChapter>, val hasNextPage: Boolean)

View File

@ -0,0 +1,25 @@
This font is © 2006 Nate Piekos. All Rights Reserved.
Created for Blambot Fonts
This font is freeware for independent comic book creation and
non-profit use ONLY. ( This excludes use by "mainstream" publishers,
(Marvel, DC, Dark Horse, Oni, Image, SLG, Top Cow, Crossgen and their
subsidiaries) without a license fee. Use by a "mainstream" publisher
(or it's employee), and use for commercial non-comic book production
(eg. magazine ads, merchandise lables etc.) incurs a license fee
be paid to the designer, Nate Piekos.
This font may not be redistributed without the author's permission and
never with this text file missing from the .zip, .sit or .hqx.
Blambot/Nate Piekos makes no guarantees about these font files,
the completeness of character sets, or safety of these files on your
computer. By installing these fonts on your system, you prove that
you have read and understand the above.
If you have any questions, visit http://www.blambot.com/license.shtml
For more free and original fonts visit Blambot.
www.blambot.com
Nate Piekos
studio@blambot.com

View File

@ -0,0 +1,93 @@
Copyright 2014 The Comic Neue Project Authors (https://github.com/crozynski/comicneue)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,202 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

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