Compare commits

...

180 Commits

Author SHA1 Message Date
Vetle Ledaal 2cfdda0bcf
Add `www` subdomain for redirecting domains (#7770)
CI / Prepare job (push) Successful in 4s Details
CI / Build individual modules (push) Failing after 11s Details
CI / Publish repo (push) Has been skipped Details
* DeviantArt - fix domain

* Kuroi Manga - fix domain

* Lelmanga - fix domain

* Mangamo - fix domain

* Manhua Zonghe - fix domain
2025-02-23 21:31:16 +00:00
Chopper d4c94d0972
ManhwaFreakXyz: Migrate theme (#7739)
* Migrate theme

* Fix lint

* Add nonce setting in the popular manga
2025-02-23 21:31:16 +00:00
Creepler13 027d09ffd9
Disasterscans: fix again (#7758)
fix again
2025-02-23 21:31:16 +00:00
Creepler13 8b86222f80
Reaperscans: fix chapterListRequest (#7748)
* fix chapterListRequest

* changes

* var

* null check does not make sense in context

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-02-23 21:31:16 +00:00
AwkwardPeak7 7210f658e2
add helper utils (#7683)
* add some helper utilities

* adjust parameter
2025-02-23 21:31:16 +00:00
dngonz fd194f60f1
Mangafire: Change type filter to multi select (#7752)
* add all option to type filter search

* change type filter to multi select
2025-02-23 21:31:16 +00:00
abubaca4 611e25e161
[RU]Nudemoon date find fix (#7750)
* Fix date find

* Version up

* Fix?
2025-02-23 21:31:16 +00:00
Creepler13 6e646630d1
Flamecomics: remove breaking Code (#7747)
* remove breaking Code

* lint
2025-02-23 21:31:16 +00:00
Chopper 82536cc63a
MysticMoon: Update domain (#7717)
* Revert "Remove Mystic Moon - parked"

This reverts commit 3b3739f14589353baaded72ce07e81239f5078db.

* Update domain

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-02-23 21:31:16 +00:00
Andika Perdana D.S. 574c64db71
Kiryuu: update base URL, version code, and NSFW status (#7581)
* Kiryuu: update base URL, version code, and NSFW status

* update base URL to new domain
2025-02-23 21:31:16 +00:00
Chopper 7bf465ddae
HadesNoFansub: Update domain (#7738)
Update domain
2025-02-23 21:31:16 +00:00
Chopper 46b8ceaecf
WeebCentral: Fix searchMangaFromElement (#7737)
Fix searchMangaFromElement
2025-02-23 21:31:16 +00:00
Creepler13 2deca0aaeb
revert RealmOasis back to Rizzfables/comics (#7722)
* revert

* version

* version id

* Update src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt

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

* keeping change

* fix comic class

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:31:16 +00:00
dngonz 8ac307c66e
Remove extensions (#7712)
remove extensions
2025-02-23 21:31:16 +00:00
Dr1ks d910910223
LibGroup: fix chapters (#7692)
* LibGroup: fix chapters

* LibGroup: review fix

* LibGroup: review fix 2

* LibGroup: lint
2025-02-23 21:31:16 +00:00
abubaca4 662da8f9fe
[RU]Nudemoon 3 fixes (#7686)
* Single date parse fix

* Fallback to single if all chapters page empty

* Fix old age titles error when domain not removed from url correctly

* Requested fix

* One more fix

* Think found all place for selectFirst

* Or one more
2025-02-23 21:31:16 +00:00
Creepler13 6149261e7b
Fix DisasterScans (#7673)
* Fix DisasterScans

* lint

* requested changes

* changes

* idk
2025-02-23 21:31:16 +00:00
Chocobo Latos 0431f55f9b
Add manga18free (#7726)
Added en/manga18free
2025-02-23 21:31:16 +00:00
dngonz 2a707c1072
MyReadingManga: Fix covers and missing chapters (#7715)
* fix covers and missing chapters

* clean spaces
2025-02-23 21:31:15 +00:00
dngonz 1585a295a3
Cosplaytele: Fix Irrelevant Search Result (#7714)
fix selector
2025-02-23 21:31:15 +00:00
Dr1ks d34316685b
Grouple: small fixes (#7693)
* Grouple: error on paid chapters

* Grouple: rating fix

* Grouple: bump

* Grouple: review fix

* Grouple: authorization check
2025-02-23 21:31:15 +00:00
Cuong-Tran 6c1e55053c
Madara: Expose genresList (#7674)
* Madara: Allow ext to preload with a list of genres

Help with fork like SY which allows saving search

* fix extensions

* update the fetchSearchManga to using HttpUrlBuilder
2025-02-23 21:31:15 +00:00
Cuong-Tran 63558a4cdb
CBHentai: fix uri path changed & search failed with special characters (#7671)
* CBHentai: fix uri path changed

also fix altNameSelector

* extract regex

* move comment

* fix search

* HttpUrlBuilder
2025-02-23 21:31:15 +00:00
Vetle Ledaal 10aa286c04
Remove 78 broken extensions (#7658)
* Remove Franxx Mangás - 403 Forbidden

* Remove MIC MIC IDOL - coming soon

* Remove Ler Yaoi - Connection timed out

* Remove Yaoi TR - Connection timed out

* Remove Hunlight Scans - Connection timed out

* Remove Manga Time - Connection timed out

* Remove Manga-Scan - Connection timed out

* Remove Momo no Hana Scan - Connection timed out

* Remove BlogTruyen.vn (unoriginal) - Connection timed out

* Remove MangaDoom - Connection timed out

* Remove MangaStorm - Connection timed out

* Remove Etheral Radiance - default cpanel

* Remove Moon Witch Scan - Domínio não encontrado

* Remove Hensekai - empty

* Remove Lunar Scans - empty

* Remove Tecno Scans - empty

* Remove Manga-Titan - empty

* Remove Lich Subs - empty

* Remove Lector Online - for sale

* Remove I Love Manhwa - Invalid SSL certificate

* Remove ManhwaNew - Invalid SSL certificate

* Remove MangaRolls - Invalid SSL certificate

* Remove Ladron Corps - Looks like this domain isn't connected to a website yet

* Remove Kishisan - no A record

* Remove MELOKOMIK - no A record

* Remove Comic Fans - no A record

* Remove MMFenix - no A record

* Remove Manga Bari - Not Found

* Remove GMANGA (unoriginal) - Origin is unreachable

* Remove ComicExtra - Origin is unreachable

* Remove Nabi Scans - Origin is unreachable

Closes #5649

* Remove MangaCV - Origin is unreachable

* Remove Fay Scans - parked

* Remove Manga-fast.com - parked

* Remove Cookie Kiara - parked

* Remove Pink Tea Comic - parked

* Remove Riot Hentai - parked

* Remove Read Goblin Slayer Manga Online - parked

* Remove Mystical Merries - parked

* Remove CopyPasteScan - parked

* Remove ManhuaChill - parked

* Remove Lolicon - parked

* Remove Mystic Moon - parked

* Remove Manga68 - parked

* Remove MyRockManga - parked

* Remove Manhua Kiss - parked

* Remove AscalonScans - parked

* Remove Doujins.lat - parked

* Remove Komik Pix - parked

* Remove MirrorDesu - parked

* Remove SISI GELAP - parked

* Remove Komiksan - parked

* Remove Pian Manga - parked

* Remove Sekaikomik - parked

* Remove My Manhwa - parked

* Remove 1st Kiss-Manga (unoriginal) - parked

* Remove Lady Manga - parked

* Remove Mangá Kun - SSL handshake failed

* Remove Oh No Manga - The connection has timed out

* Remove Dark-Scan.com - The connection has timed out

* Remove Babel Wuxia - The connection has timed out

* Remove Mangazavr - The connection has timed out

* Remove Read Noblesse Manhwa Online - Unable to connect

* Remove Nirvana Scan - Unable to connect

* Remove Mantraz Scan - Unable to connect

* Remove SCARManga - Unable to connect

* Remove Gatemanga - unrelated

* Remove ManhwaBookShelf - unrelated

* Remove Read Tower of God Manhwa Manga Online - unrelated

* Remove Kofi Scans - Web server is down

* Remove Nyrax Manga - Web server is down

* Remove YD-Comics - Web server is down

* Remove AnonimusTLS - Web server is down

* Remove Mangaland - Website not found...

* Remove MangaNoon - We're having trouble finding that site

Closes #7042

* Remove NoonScan - We're having trouble finding that site

Closes #7042

* Remove SobatManKu - Your domain is expired

* Remove Comic 21 - Your domain is expired
2025-02-23 21:31:15 +00:00
Chopper 94603b3843
QuantumScans: Migrate theme (#7708)
Migrate theme
2025-02-23 21:31:15 +00:00
Chopper 6fa43de2c5
MTL: Improves readability (#7682)
* Improves readability

* Update settings messages
2025-02-23 21:31:15 +00:00
Chopper b347869525
SamuraiScan: Update domain (#7680)
Update domain
2025-02-23 21:31:15 +00:00
Chopper 69268fc1e3
Bakai: Fix selector (#7679)
Fix selector
2025-02-23 21:31:15 +00:00
Chopper 3a3cdded16
Add GekkouScans (#7678) 2025-02-23 21:31:15 +00:00
bapeey 165c752b5b
Comico: Replace `Error` with `Exception` (#7672)
replace error with exception
2025-02-23 21:31:15 +00:00
are-are-are 936331464b
TeamLanhLung & Manhuarock & TruyenTranhDamMy Update domain (#7670)
* Manhuarock update domain

* TruyenTranhDamMy update domain

* TeamLanhLung update domain
2025-02-23 21:31:15 +00:00
Fioren c9bd7e0825
DocTruyen3Q: Update domain (#7669)
Update domain DocTruyen3Q
2025-02-23 21:31:15 +00:00
are-are-are 7c55e0b5ae
Toptruyen & TruyenVN Update domain (#7664)
* TruyenVN update domain

* TopTruyen update domain
2025-02-23 21:31:15 +00:00
Vetle Ledaal b7acdd9390
NineHentai: do not use `* -1` (#7662) 2025-02-23 21:31:15 +00:00
Xyndra 1455398b8f
Fix Realm Oasis (#7650)
* Fix Realm Oasis

* Move back to `rizzcomic`

* Make referrer dynamic

* Keep "X-API-Request"
2025-02-23 21:31:15 +00:00
Creepler13 e079bf1f88
Philliascans change to madara (#7648)
* Philliascans change to madara

* Bump versionID

* lint

* Update src/en/philiascans/src/eu/kanade/tachiyomi/extension/en/philiascans/PhiliaScans.kt

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-02-23 21:31:15 +00:00
dngonz 7408c2e05a
DragonTranslation.net (ES): Migrate to HttpSource (#7629)
* fix extension

* fix details

* migrate to ParsedHttpSource

* migrate to HttpSource

* Update src/es/dragontranslationnet/src/eu/kanade/tachiyomi/extension/es/dragontranslationnet/DragonTranslationNet.kt

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-02-23 21:31:15 +00:00
dngonz 78a055915d
Remove extensions (#7656)
remove
2025-02-23 21:31:15 +00:00
dngonz ebd8d72ada
Ikigai Mangas: Change domain (#7657)
change domain
2025-02-23 21:31:15 +00:00
bapeey dd47360020
MangaReader: Fix crash on SY (#7661)
fix crash on sy
2025-02-23 21:31:15 +00:00
Creepler13 54ca7ab4a3
HiveScans/Toons: Fix PageListParse (#7640)
* Fix PageListParse

* change to json
2025-02-23 21:31:15 +00:00
mrtear a670f5d4ec
Add DxdScans (#7616)
DxD

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:31:15 +00:00
Prem Kumar 86cdc76645
Removed Manga Flame(AR), added Flone Scans(EN) (#7608)
* MangaFlame(ar) to FloneScans(en)

* suggested change

* Update build.gradle

bump reset

* Update FloneScans.kt

new line
2025-02-23 21:31:15 +00:00
Secozzi 1b1ef9274b
fix(en/mangafire): rework mangafire extension (#7625)
* fix(en/mangafire): rework mangafire extension

* oops

* remove non-null assert

* small fixes
2025-02-23 21:31:15 +00:00
Smol Ame 17300b3bd0
ManhwaDesu: Update domain URL (#7638)
* ManhwaDesu: Bump versionCode

* ManhwaDesu: Update baseURL

* ManhwaDesu: Update icons
2025-02-23 21:31:15 +00:00
bapeey 2d2ba69ee9
Remove MangaMate (#7639)
remove mangamate
2025-02-23 21:31:15 +00:00
Hellkaros ade7d90d51
Yushuke Mangas: Fix popular titles && update logo (#7622)
fix: popular titles && update logo
2025-02-23 21:31:15 +00:00
dngonz cc401ccf80
Remove 18kami (#7623)
remove kami18
2025-02-23 21:31:15 +00:00
are-are-are 94aeab80fa
VlogTruyen: update domain & fix pageListParse & Use a clear logo (#7620)
* update domain & update pageListParse

* Use a clear logo
2025-02-23 21:31:15 +00:00
Prem Kumar 7c3386fcf2
Manga Gezgini(tr): Update Domain, and DoujinDesu (id): Fix typo in Genre (#7619)
* update domain MangaGezgini

* fix typo
2025-02-23 21:31:15 +00:00
Dr1ks 5e57723c32
Grouple: Add user token (#7617)
* Grouple: Add user token

* Grouple: Bump

* Grouple: update readmanga domain
2025-02-23 21:31:15 +00:00
Hellkaros bd311d42e8
DreamScan: Fix domain (#7610)
fix: domain
2025-02-23 21:31:15 +00:00
Secozzi 8ae19659f0
feat(en): add manganow (#7600) 2025-02-23 21:31:15 +00:00
Secozzi 56d872d023
feat(lib-multisrc/mangareader): Rework mangareader (#7561)
* chore: move mangafire away from mangareader multisrc

* chore: rework mangareader multisrc theme

* lint(lint): lint

* fix: apply recommended fixes

* lint(lint): lint

* bump versions
2025-02-23 21:31:15 +00:00
mrtear 2a44209a2f
weebcentral: add extra info, format manga details (#7586)
* weebcentral: format

* apply suggestions

* f

* Update WeebCentral.kt

* Update WeebCentral.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:31:15 +00:00
AwkwardPeak7 0f4d62d622
RealmOasis: remove 0ms (#7591)
they would rather get ddosed by tachi users directly than let a caching
proxy do its thing
2025-02-23 21:31:15 +00:00
mrtear fa1562218c
Add GodDoujin [th] (#7587) 2025-02-23 21:31:15 +00:00
Hellkaros d438400491
Add SafireScan (#7575) 2025-02-23 21:31:15 +00:00
Hellkaros 7926ca6011
Yushuke Mangas: Fix latest titles (#7572)
fix: latest titles
2025-02-23 21:31:15 +00:00
Chopper 11d6ca37c3
Snowmtl: Add support to Google Translator (#7558)
* Add support to Google Translator

* Remove Request.Builder

* Remove unused code

* Refatoring

* Replace the hashmap with the when statement

* Improves readability

* Remove site color and add text outline
2025-02-23 21:31:14 +00:00
scb261 58941d9440
Weeb Central: specify offical and unofficial scans (#7566) 2025-02-23 21:31:14 +00:00
bapeey 732949510a
Catharsis World: Update domain and theme (#7565)
* update theme

* fix chapterlist edge case
2025-02-23 21:31:14 +00:00
are-are-are 72665ad48c
TruyenGG & TruyenQQ Fix image slow loading (#7564)
* TruyenGG Fix image slow loading

* TruyenQQ Fix image slow loading
2025-02-23 21:31:14 +00:00
bapeey ad58836d20
Visor Inari: Update domain and theme (#7563)
* change theme

* add genre selector

* fix chapterlist edge case
2025-02-23 21:31:14 +00:00
Prem Kumar a4934b8529
Manhwa Clan: Update Nsfw status and Icons (#7555)
* update icons

* update version code

* nsfw

* update icons
2025-02-23 21:31:14 +00:00
Prem Kumar 469e9839e8
Kai Scans: Update domain, name and icons (#7554)
* Update domain, name and icons

* delete override value id
2025-02-23 21:31:14 +00:00
Prem Kumar 5c73069818
Cypher Scans: Update Domain and Icons (#7552)
Update domain and icons
2025-02-23 21:31:14 +00:00
mrtear 28942d4f8f
Add AeinScans (#7550) 2025-02-23 21:31:14 +00:00
mrtear de1914c2fa
RezoScans: Fix Null Tag (#7549)
RezoScan: Fix Null Tag
2025-02-23 21:31:14 +00:00
Prem Kumar f47d81ba39
Removed Aniglis Novels (#7548) 2025-02-23 21:31:14 +00:00
Prem Kumar 8bb0acfc1d
Animated Glitched Comics: updated icons (#7545)
change icon
2025-02-23 21:31:14 +00:00
mrtear 16a8b7f758
RichtoScan: Update Domain (#7536)
* Bump

* Update URL
2025-02-23 21:31:14 +00:00
Hellkaros b5e9d35b0b
HentaiDexy: Fix domain and API request (#7530)
fix: domain and API request
2025-02-23 21:31:14 +00:00
Chopper 643376e8a0
Snowmlt: Add support to arabic (#7514)
* Add support to arabic

* Refatoring
2025-02-23 21:31:14 +00:00
Hellkaros d6e5553084
Yushuke Mangas: Fix chapters list and pages (#7504)
fix: chapters list and pages
2025-02-23 21:31:14 +00:00
Hellkaros a473bfca82
NekoScans: Fix selectors and domain (#7404)
fix: selectors and domain
2025-02-23 21:31:14 +00:00
AwkwardPeak7 3a10804380
change to our fork of extensions-lib (#7539) 2025-02-23 21:31:14 +00:00
dngonz db4e03c62e
Remove ReadcomicNet (#7521)
remove
2025-02-23 21:31:14 +00:00
Hellkaros 40efcf1be8
BatCave: Fix page url (#7529)
fix: page url
2025-02-23 21:31:14 +00:00
Hellkaros 4154954883
Domain update (#7528) 2025-02-23 21:31:14 +00:00
adeehilman 551b09d0d6
fix domain ikiru (#7522) 2025-02-23 21:31:14 +00:00
dngonz b4f3f5f565
ManhuaRock: Change domain and fix pages (#7520)
change domain and fix pages
2025-02-23 21:31:14 +00:00
Hellkaros 2d6ded730e
Norte Rose Scan: Fix domain (#7513)
fix: domain
2025-02-23 21:31:14 +00:00
Hellkaros a622437064
Read Mangas: Fix referer encode (#7512)
fix referer encode
2025-02-23 21:31:14 +00:00
Smol Ame a55bcf4996
Manga Galaxy: Remove extension (#7510)
Manga Galaxy: Remove extension files
2025-02-23 21:31:14 +00:00
bapeey 2147b87816
LectorTmo: Remove LectorManga and fix crash on some devices (#7508)
* override fetch methods

* remove lectorManga
2025-02-23 21:31:14 +00:00
AlphaBoom 7dd76fe0c9
MangaGun: Fix image load (#7501) 2025-02-23 21:31:14 +00:00
AlphaBoom 4b90bdeb0c
Manhuagui: Add Ranking Filter (#7499)
* Manhugui: Add Ranking Filter

* Manhugui: Fix original sort function

* Manhuagui: Correct the file name
2025-02-23 21:31:14 +00:00
heddxh 0d94dabe27
Add ComicGrowl (#7489)
* comicgrowl: add new source

* comicgrowl: add locked status in chapterlist

* comicgrowl: fetch latest mangas
2025-02-23 21:31:14 +00:00
dngonz 9cfd561677
Jiangzaitoon: Fix domain (#7497)
fix domain
2025-02-23 21:31:14 +00:00
AwkwardPeak7 42d3545a0a
RizzComic/RealmOasis: domain change + support random urls (#7487)
* rizz: random urls go brrr

* bump, rename, get*Url

* icon

* 0ms?

* remove comments

* regex init in object, comment unused function for future

* lint

* suggestions

* pref
2025-02-23 21:31:14 +00:00
spicemace 6e7fcda20c
Mangadex markdown cleanup removes --- only with newline (#7282)
* Update MangaDexHelper.kt

* Update build.gradle

* Update build.gradle
2025-02-23 21:31:14 +00:00
mrtear 1d24591d65
Add KenScans (#7483) 2025-02-23 21:31:14 +00:00
Vetle Ledaal b6194e68b3
Remove domains with no A record (#7481) 2025-02-23 21:31:14 +00:00
mrtear f6b09f2a03
Add ValirScans (#7473) 2025-02-23 21:31:14 +00:00
Chopper 0505a26934
MachineTranslations & Snowmtl: Fix the regex in the translator engine and add font size settings (#7465)
* Fix the regex in the translator engine and add font size settings

* Remove extra lines

* Remove rateLimit

* Remove init pref

* Use lazy statement in the snowmtl client

* Relax the exception and show pages without dialog

* Fix the translator's bad formatted response for some cases

* Change listener return to false

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:31:14 +00:00
Vetle Ledaal e080f6fd1b
Siikomik: update domain (#7476) 2025-02-23 21:31:14 +00:00
Vetle Ledaal 05f5fa8df5
Raijin Scans (Madara): update domain (#7475)
Raijin Scans: update domain
2025-02-23 21:31:14 +00:00
Vetle Ledaal b6ed7ac63b
Madara: fix status with spaces (#7474) 2025-02-23 21:31:14 +00:00
heddxh c4c6fe69f4
Manhuagui: Fix single quote parsing (#7471)
* manhuagui: convert single quote before unpack

* manhuagui: bump extension version to 21
2025-02-23 21:31:14 +00:00
AwkwardPeak7 0658c1926c
revert kotlin 2.1.0 and deps for now (#7468)
generated serializers seem to be missing in final apk, need further investigation
2025-02-23 21:31:11 +00:00
AwkwardPeak7 2e45708568
fix lint 2025-02-23 21:30:42 +00:00
Chopper e2c7543d3e
SussyToons: Fix thumbnail, loading pages and update domain (#7461)
* Fix thumbnail, loading pages and update domain

* Handling JSON chunks with json lib
2025-02-23 21:30:42 +00:00
Chopper 3403b7a0ec
Retsu: Fix search manga selector (#7456)
Fix search manga url selector
2025-02-23 21:30:42 +00:00
Chopper 48ed9621c7
HattoriManga: Fix search and manga status (#7455)
Fix search and manga status
2025-02-23 21:30:41 +00:00
Chopper 831cc82113
LuaScans: Migrate theme (#7454)
Migrate theme
2025-02-23 21:30:41 +00:00
Chopper 0a3f608727
SirenKomik: Fix loading pages (#7453)
Fix loading pages
2025-02-23 21:30:41 +00:00
Vetle Ledaal deb32e6f28
Remove some parked and available domains (#7450)
Remove parked and available domains

BestManhua
Girls Love Manga!
Komik Lab
Lala Manga
Manga Weebs
VinManga
AiYuManhua
Manga Latino
FR-Scan
Indo18h
KomikGan
Komik Say
WorldManhwas
2025-02-23 21:30:41 +00:00
Vetle Ledaal 9a44b80cb5
Coffee Manga: use correct chapter pages URL (#7444)
* Coffee Manga: use correct chapter pages URL

* simplify
2025-02-23 21:30:41 +00:00
are-are-are b9c2949bf6
Update some domain (#7359)
* YuriNeko Update domain and rewrite to overwrite correct domain

* TruyenVN update domain

* HentaiVNPlus update domain

* YuriNeko Use wholeText()

* TeamLanhLung update domain

* LxHentai update domain

* Yurineko Revert code and apply suggestion

* LxHentai use wholeText()
2025-02-23 21:30:41 +00:00
Bui Dai 3a8b7c697e
Add TruyenTranh3Q (#7300)
* Add TruyenTranh3Q

* fix: improve null safety for next.js image URL decoding

* fix(search): combine filters with search queries

the website backend supports filters parameters being used alongside
search queries despite the UI not showing filter control during text
search, this matches actual API behavior observed through direct URL
teseting

* remove URLDecoder part

* remove non-null assert for date_upload

* use absUrl

* use build()

* remove try/catch for parseRelativeDate
2025-02-23 21:30:41 +00:00
AwkwardPeak7 74145d9a55
[CI] generate matrixes: correctly include deleted modules when core
project files change
2025-02-23 21:30:38 +00:00
renovate[bot] 9b19e756cb
Update dependency gradle to v8.12.1 (#7345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-23 21:30:05 +00:00
AwkwardPeak7 4867bf18a0
Update to Kotlin 2.1.0 and add spotless linter (#7447)
* update kotlin & deps

* add spotless

* fetch depth

* lint...

* adjust rules

* Revert "lint..."

This reverts commit cc2f99fb218726d90045c5104ab9592a179cb6b6.

* lint

* rm generated file
2025-02-23 21:29:55 +00:00
Pavka 86a24181b5
[ru-Acomics] Fix parsing popular and search page (#6611)
* [ru-Acomics] Fix parsing popular and search page

* Update AComics.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:27:25 +00:00
kana-shii 5ad751cc20
Yaoi Manga Online: Remove author name from title (#7016)
* Update YaoiMangaOnline.kt

* Update build.gradle

* Update YaoiMangaOnline.kt

* Update YaoiMangaOnline.kt

* Update YaoiMangaOnline.kt

* Update YaoiMangaOnline.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:27:25 +00:00
Eshlender 28f152a029
[RU]Desu new domain (#7283)
* [RU]Desu new domain

* no lazy

* Update Desu.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-02-23 21:27:25 +00:00
Vetle Ledaal 7c7553dec1
Hitomi: refresh expiring page URLs (#7440) 2025-02-23 21:26:50 +00:00
dngonz 83145f221f
Fix domains (#7427)
fix domains
2025-02-23 21:26:50 +00:00
Smol Ame eb07f68d63
Mangasco: Remove extension (#7432) 2025-02-23 21:26:50 +00:00
Smol Ame c902a0eb3a
Ignis Comic: Switch to Manhuaga URL (#7421)
* Ignis Comic: Bump versionCode

* Ignis Comic: Use current domain URL
2025-02-23 21:26:49 +00:00
bapeey 32af52d14f
MangaEsp: Update domain and change theme (#7418)
update url and theme
2025-02-23 21:26:49 +00:00
bapeey bf3e698542
Add CatManhwas (#7416)
* add catmanhwas

* add nsfw
2025-02-23 21:26:49 +00:00
Altometer 1bd2f6cd65
Zenko: Fixed loading manga images on page (#7413) 2025-02-23 21:26:49 +00:00
dngonz bc0b80fc6f
Reaper Scans (unoriginal): Fix no pages found (#7411)
fix pages
2025-02-23 21:26:49 +00:00
Hellkaros 2596ddea63
KomikCast: Domain update (#7410)
Domain update
2025-02-23 21:26:49 +00:00
dngonz c9010a0167
Komiku.com: Fix url (#7408)
change domain
2025-02-23 21:26:49 +00:00
Hellkaros 9a7eaafc20
Yushuke Mangas: Fix chapters list (#7400)
fix: chapters list
2025-02-23 21:26:49 +00:00
dngonz f46b854fad
Taurus Fansub: Fix domain (#7396)
* fix domain

* fix gradle
2025-02-23 21:26:49 +00:00
DokterKaj 3218b4370f
Batoto: Make statuses null if not exists (#7378)
Make statuses null if not exists
2025-02-23 21:26:49 +00:00
DokterKaj 487ed3fb38
DeviantArt: Add preference for artist name in manga title (#7375)
* Add preference for artist name in manga title

* Move || to end of previous line

* Remove redundant function definitions

* Rename pref entries, clarify summary, make pageListParse readable
2025-02-23 21:26:49 +00:00
Hellkaros 343024eb03
Domain update (#7370)
* Domain update

* fix addheader
2025-02-23 21:26:49 +00:00
Hellkaros 404bb0dd8c
Add yomu (#7368)
Add yomu comics
2025-02-23 21:26:49 +00:00
are-are-are f4780ecb11
GocTruyenTranh edit 2 search filter (#7339)
* Update filter

* Update GocTruyenTranh.kt
2025-02-23 21:26:49 +00:00
SonoPG 701b46e29a
MangaWorld: Fix MWCookie requirement (#7319)
* Create ShittyRedirectionInterceptor.kt

* Add redirection interceptor

* Update version code

* Don't be lazy

* Avoid NullPointerExceptions when searching for MWCookie

* Update MangaWorld.kt with new name for interceptor

* Rename class and store regex somewhere else

* Check for content-type instead of header and return res correctly

* Use .contains to not fail on longer types

* Revert "vary" method to check for the JS challenge

The "text/html" method breaks way too easily, the response.headers["vary"] just works fine.
2025-02-23 21:26:49 +00:00
Vetle Ledaal a0e3441507
Bato.to: update mirrors, pick one at random, deprecated mirror migration (#7317)
* Bato.to: update mirrors, pick one at random

* add 'auto' mirror option

* Do not use a deprecated mirror, even if explicitly set
2025-02-23 21:26:49 +00:00
spicemace 7722a6e458
Kemono fix search pages (#7279)
* Kemono fix search pages

* Update build.gradle.kts

* Update Kemono.kt
2025-02-23 21:26:49 +00:00
Dr1ks 25bac4f262
Nudemoon: New domain (#7311)
* Nudemoon: New domain

* Nudemoon: review fix
2025-02-23 21:26:49 +00:00
dngonz 43979e6f99
LeerCapitulo: Fix unable to find the script (#7379)
fix selector
2025-02-23 21:26:49 +00:00
dngonz 1fb7d1c976
Reaper Scans (unoriginal): Fix date parse (#7376)
fix date parse
2025-02-23 21:26:49 +00:00
Hellkaros 9a6a106b38
Add spectral (#7367) 2025-02-23 21:26:49 +00:00
Hellkaros e6753d4ea5
Remove tsuki (#7366) 2025-02-23 21:26:49 +00:00
dngonz ed426f5e6b
Hentaidex: Fix manga directory (#7362)
fix manga directory
2025-02-23 21:26:49 +00:00
dngonz 5fd5b76527
MadTheme: Fix chapter client (#7361)
fix and try to limit api calls
2025-02-23 21:26:49 +00:00
Cuong-Tran 448956e853
coffeemanga: fix loading image in old chapters (#7353) 2025-02-23 21:26:49 +00:00
Vetle Ledaal 4500fc3584
Colamanga: fix keyType not found (#7352) 2025-02-23 21:26:49 +00:00
Dr1ks f632d3636f
Mangahub (RU): Fixex (#7321)
* Mangahub: Fixes

* Mangahub: Fix status

* Mangahub: Fix search

* Mangahub: review fix
2025-02-23 21:26:49 +00:00
Chopper bfde21aa95
Bakai: Fix path segment (#7318)
* Fix path segment

* Restricts specific origin paths in outdated path resolution
2025-02-23 21:26:49 +00:00
AwkwardPeak7 932ce83b1c
IRovedOut: fix regex 2025-02-23 21:26:49 +00:00
ItsLogic d65dce62ae
WeebCentral: Fix No Chapters Found (#7342) 2025-02-23 21:26:49 +00:00
Chopper 8057b3f55a
EZmanga: Migrate theme (#7330)
Migrate theme
2025-02-23 21:26:49 +00:00
Vetle Ledaal c15852943e
Remove named capture groups from most extensions (#7328)
* remove named capture group (simple)

* remove named capture group (spot checks)
2025-02-23 21:26:49 +00:00
DokterKaj 07c6de7cf6
MangaDex: Show final chapter in description (#7314)
* Show final chapter in description

* Make it a preference
2025-02-23 21:26:49 +00:00
DokterKaj 944586408e
Batoto: Fix extra info + Hide empty alt titles + Fall back to upload status (#7312)
Fix extra info + Hide empty alt titles + Fall back to upload status
2025-02-23 21:26:49 +00:00
DokterKaj b0eda7b44d
DeviantArt: Support multi-image posts (#7305)
* Support multi-image posts

* Get substring before "/v1/" for better reliability

* Minor tweaks
2025-02-23 21:26:49 +00:00
Bui Dai bc6e37008b
Update senshi manga domain (#7316)
update senshi manga domain
2025-02-23 21:26:49 +00:00
Dr1ks 0a0ba8ea6c
Remove dead extension (#7322)
Removed dead extension
2025-02-23 21:26:49 +00:00
dngonz 8424084c58
Emperor Scan: Fix domain (#7303)
fix domain
2025-02-23 21:26:49 +00:00
Chopper 24aa61ed77
SussyToons: Fixes (#7294)
Fixes
2025-02-23 21:26:48 +00:00
Chopper ff790752e0
Bakai: Fix path (#7293)
Fix path
2025-02-23 21:26:48 +00:00
Hellkaros 48913cde0a
Remove Extensions (#7269) 2025-02-23 21:26:48 +00:00
dngonz 08ff6b51b3
Mangahentai: use old madara search (#7265)
use old madara search
2025-02-23 21:26:48 +00:00
Dr1ks 0637c0d0eb
AllManga: Fix manga and chapter URLs (#7258)
* AllManga: Fix manga and chapter URLs

* Revert "AllManga: Fix manga and chapter URLs"

This reverts commit 6b8bda5a405f108d2076c9b650b96398bae723d0.

* AllManga: Fix manga and chapter URLs 2
2025-02-23 21:26:48 +00:00
Hellkaros 20b51fa15e
Read Mangas: Fix description (#7249)
fix: description
2025-02-23 21:26:48 +00:00
are-are-are a25c0555d8
VlogTruyen Update Domain & update logo & change name (#7244)
* VlogTruyen Update Domain

* Update Logo and change extensions name the same name source

* Revert

* override id

* Update Logo ~~
2025-02-23 21:26:48 +00:00
Altometer 4c3c2212e1
Add Zenko (#7185)
* Add Zenko

* Apply requested changes

* Apply requested changes

* Added right url for opening manga on webview

* Apply kotlin lint

* Use HttpUrl for parsing ids & Added right url for chapter

* Apply requested changes

* Fixed generate id if chapter contains dot

* Apply requested changes
2025-02-23 21:26:48 +00:00
dngonz 3f86aa1c40
Madtheme: fix chapters fetching (#7270)
* add chapters fetch preferences

* bump

* remove fetching preferences and call both enpoints

* remove const

* fix
2025-02-23 21:26:48 +00:00
dngonz 936a7f1fde
MadTheme: Enhance chapter parse (#7214)
enhance chapter parse
2025-02-23 21:26:48 +00:00
Chopper c063dda9f0
Add MangaHosted (#7259)
* Add MangaHosted

* Fix search request
2025-02-23 21:26:48 +00:00
dngonz 24772e3262
MurimScan: fix mangaSubString (#7243)
* fix mangaSubString

* fix baseUrl
2025-02-23 21:26:48 +00:00
Hellkaros 21ceea01e9
AlgodaoDoce: Fix status (#7222)
fix: status
2025-02-23 21:26:48 +00:00
are-are-are efdaf2f6f5
Add GocTruyenTranh (#7205)
* Add Goctruyentranh

* Fix duplicate title latestUpdates&popularManga

* Fix lint & add chapter date upload and add change requests

* case insensitive

* use selectFirst for chapterFromElement

* Use .selectFirst() title & update parseDate

* Use Calendar
2025-02-23 21:26:48 +00:00
AwkwardPeak7 99c85784e3
move some source files to standard location (#7247)
* move files to standard locations

* format

* package

* lint
2025-02-23 21:26:48 +00:00
dngonz efc7e0f088
Madara: fix authors and/or status (#7242)
* fix authors and status

* fix chapter order for nocturnesummer
2025-02-23 21:26:48 +00:00
Aurel c4a3d0c39e
Madara: Improve French relative date parsing (#7161)
Enhance date parsing for "jour" format in Madara library
2025-02-23 21:26:48 +00:00
Hellkaros dd96377d74
Azora: Fix description text (#7219)
* fix: description text

* fix: Shortening the CSS selector
2025-02-23 21:26:48 +00:00
Chopper 4193a3e304
Remove VapoScans (#7218) 2025-02-23 21:26:48 +00:00
Chopper 04fce0106a
ReadMangas: Add rate limit (#7217)
Add rate limit
2025-02-23 21:26:48 +00:00
Chopper f9996f5921
Crystal: Fix image loading (#7200)
* Crystal: Fix image loading

* Merge Etoshore into CrystalComics
2025-02-23 21:26:48 +00:00
dngonz 05c0051c32
MyHentaiComics: fix extension (#7198)
* MyHentaiComics: fix extension

* fix

* use httpurl builder

* fetch only once
2025-02-23 21:26:48 +00:00
1466 changed files with 5998 additions and 15128 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ repo/
apk/
gen
generated-src/
.kotlin

View File

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

View File

@ -38,6 +38,7 @@ kotlinter {
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
implementation(project(":utils"))
}
tasks {
@ -51,3 +52,9 @@ tasks {
}
}
}
tasks.register("printDependentExtensions") {
doLast {
project.printDependentExtensions()
}
}

View File

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

View File

@ -9,7 +9,7 @@ gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
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" }

View File

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

View File

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

View File

@ -276,14 +276,14 @@ abstract class ColaManga(
}.also(screen::addPreference)
}
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(?<keyType>\d+)\s*\)\s*\{\s*return\s*'(?<key>[a-zA-Z0-9_]+)'\s*;""")
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(\d+)\s*\)\s*\{\s*return\s*'([a-zA-Z0-9_]+)'\s*;""")
private val keyMapping by lazy {
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
?: throw Exception(intl.couldNotDeobufscateScript)
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
keyMappingRegex.findAll(readJs).associate { it.groups[2]!!.value to it.groups[3]!!.value }
}
private fun randomString() = buildString(15) {

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

@ -129,7 +129,7 @@ abstract class GroupLe(
infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>")
.substringAfterLast(": <b>").replace(",", ".").toFloat() * 2
val ratingVotes =
infoElement.select(".col-sm-7 .user-rating meta[itemprop=\"ratingCount\"]")
infoElement.select(".col-sm-6 .user-rating meta[itemprop=\"ratingCount\"]")
.attr("content")
val ratingStar = when {
ratingValue > 9.5 -> "★★★★★"
@ -209,14 +209,16 @@ abstract class GroupLe(
}
protected open fun getChapterSearchParams(document: Document): String {
return "?mtr=true"
val scriptContent = document.selectFirst("script:containsData(user_hash)")?.data()
val userHash = scriptContent?.let { USER_HASH_REGEX.find(it)?.groupValues?.get(1) }
return userHash?.let { "?d=$it&mtr=true" } ?: "?mtr=true"
}
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val document = response.asJsoup()
if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
}
@ -309,7 +311,7 @@ abstract class GroupLe(
val html = document.html()
if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
@ -322,6 +324,9 @@ abstract class GroupLe(
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
}
else -> {
if (document.selectFirst("div.alert") != null || document.selectFirst("form.purchase-form") != null) {
throw Exception("Эта глава платная. Используйте сайт, чтобы купить и прочитать ее.")
}
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
}
}
@ -436,5 +441,6 @@ abstract class GroupLe(
private const val UAGENT_TITLE = "User-Agent(для некоторых стран)"
private const val UAGENT_DEFAULT = "arora"
const val PREFIX_SLUG_SEARCH = "slug:"
private val USER_HASH_REGEX = "user_hash.+'(.+)'".toRegex()
}
}

View File

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

View File

@ -105,7 +105,7 @@ class HeanCmsChapterDto(
@SerialName("chapter_name") private val name: String,
@SerialName("chapter_title") private val title: String? = null,
@SerialName("chapter_slug") private val slug: String,
@SerialName("created_at") private val createdAt: String,
@SerialName("created_at") private val createdAt: String? = null,
val price: Int? = null,
) {
fun toSChapter(

View File

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

View File

@ -107,7 +107,7 @@ open class Kemono(
}
var mangas = mangasCache
if (page == 1) {
if (page == 1 || mangasCache.isEmpty()) {
var favourites: List<KemonoFavouritesDto> = emptyList()
if (fav != null) {
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
@ -132,7 +132,7 @@ open class Kemono(
includeType && !excludeType && isFavourited &&
regularSearch
}.also { mangasCache = mangas }
}.also { mangasCache = it }
}
val sorted = when (sort.first) {

View File

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

View File

@ -290,7 +290,7 @@ abstract class Keyoapp(
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
?.let {
val cdnHost = CDN_HOST_REGEX.find(it.html())
?.groups?.get("host")?.value
?.groups?.get(1)?.value
?.replace(CDN_CLEAN_REGEX, "")
"https://$cdnHost/uploads"
}
@ -314,7 +314,7 @@ abstract class Keyoapp(
protected open fun Element.getImageUrl(selector: String): String? {
return this.selectFirst(selector)?.let { element ->
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
IMG_REGEX.find(element.attr("style"))?.groups?.get(1)?.value
?.toHttpUrlOrNull()?.let {
it.newBuilder()
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
@ -376,8 +376,8 @@ abstract class Keyoapp(
companion object {
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//([^/]+)""".toRegex()
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
val IMG_REGEX = """url\(['"]?([^(['"\)])]+)""".toRegex()
}
}

View File

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

View File

@ -313,37 +313,33 @@ abstract class LibGroup(
override fun chapterListParse(response: Response): List<SChapter> {
val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
val chaptersData = response.parseAs<Data<List<Chapter>>>()
if (chaptersData.data.isEmpty()) {
throw Exception("Нет глав")
}
.also { if (it.data.isEmpty()) return emptyList() }
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
val defaultBranchId = if (chaptersData.data.getBranchCount() > 1) { // excess request if branchesCount is only alone = slow update library witch rateLimitHost(apiDomain.toHttpUrl(), 1)
val defaultBranchId = if (sortingList == "ms_mixing" && chaptersData.data.getBranchCount() > 1) {
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
} else {
null
}
val chapters = mutableListOf<SChapter>()
for (it in chaptersData.data.withIndex()) {
if (it.value.branchesCount > 1) {
for (currentBranch in it.value.branches.withIndex()) {
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
}
} else if (sortingList == "ms_combining") { // ms_combining
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
return chaptersData.data.flatMap { chapter ->
when {
chapter.branchesCount > 1 && sortingList == "ms_mixing" -> {
val branch = chapter.branches
.firstOrNull { it.branchId == defaultBranchId }?.branchId
?: chapter.branches.first().branchId
listOf(
chapter.toSChapter(slugUrl, branch, isScanUser()),
)
}
chapter.branchesCount > 1 && sortingList == "ms_combining" -> {
chapter.branches.map { branch ->
chapter.toSChapter(slugUrl, branch.branchId, isScanUser())
}
}
} else {
chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
else -> listOf(chapter.toSChapter(slugUrl, isScanUser = isScanUser()))
}
}
return chapters.reversed()
}.reversed()
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {

View File

@ -0,0 +1,6 @@
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

@ -0,0 +1 @@
font_size_title=Tamaño de letra

View File

@ -0,0 +1 @@
font_size_title=Taille de la police

View File

@ -0,0 +1 @@
font_size_title=Ukuran font

View File

@ -0,0 +1 @@
font_size_title=Dimensione del carattere

View File

@ -0,0 +1,6 @@
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

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

View File

@ -1,9 +1,18 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Application
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
@ -15,21 +24,26 @@ 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.Injekt
import uy.kohesive.injekt.api.get
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,
val language: Language,
) : ParsedHttpSource() {
private val language: Language,
) : ParsedHttpSource(), ConfigurableSource {
override val supportsLatest = true
@ -37,9 +51,66 @@ abstract class MachineTranslations(
override val lang = language.lang
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
.build()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
/**
* 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 ===============================
@ -203,9 +274,76 @@ abstract class MachineTranslations(
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

@ -2,4 +2,18 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations
class MachineTranslationsFactoryUtils
data class Language(val lang: String, val target: String = lang, val origin: String = "en")
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

@ -26,16 +26,13 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import kotlin.math.pow
import kotlin.math.sqrt
// The Interceptor joins the dialogues and pages of the manga.
@RequiresApi(Build.VERSION_CODES.O)
class ComposedImageInterceptor(
baseUrl: String,
val language: Language,
var language: Language,
) : Interceptor {
private val json: Json by injectLazy()
@ -55,7 +52,7 @@ class ComposedImageInterceptor(
}
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
?: throw IOException("Dialogues not found")
?: emptyList()
val imageRequest = request.newBuilder()
.url(url)
@ -63,7 +60,9 @@ class ComposedImageInterceptor(
// Load the fonts before opening the connection to load the image,
// so there aren't two open connections inside the interceptor.
loadAllFont(chain)
if (language.disableSourceSettings.not()) {
loadAllFont(chain)
}
val response = chain.proceed(imageRequest)
@ -78,9 +77,9 @@ class ComposedImageInterceptor(
dialogues.forEach { dialog ->
val textPaint = createTextPaint(selectFontFamily(dialog.type))
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
val dialogBox = createDialogBox(dialog, textPaint)
val y = getYAxis(textPaint, dialog, dialogBox)
canvas.draw(dialogBox, dialog, dialog.x1, y)
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, y)
}
val output = ByteArrayOutputStream()
@ -104,7 +103,7 @@ class ComposedImageInterceptor(
}
private fun createTextPaint(font: Typeface?): TextPaint {
val defaultTextSize = 24.pt // arbitrary
val defaultTextSize = language.fontSize.pt
return TextPaint().apply {
color = Color.BLACK
textSize = defaultTextSize
@ -116,6 +115,10 @@ class ComposedImageInterceptor(
}
private fun selectFontFamily(type: String): Typeface? {
if (language.disableSourceSettings) {
return null
}
if (type in fontFamily) {
return fontFamily[type]?.second
}
@ -206,7 +209,7 @@ class ComposedImageInterceptor(
}
}
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout {
var dialogBox = createBoxLayout(dialog, textPaint)
/**
@ -217,18 +220,8 @@ class ComposedImageInterceptor(
dialogBox = createBoxLayout(dialog, textPaint)
}
// Use source setup
if (dialog.isNewApi) {
textPaint.color = dialog.foregroundColor
textPaint.bgColor = dialog.backgroundColor
textPaint.style = if (dialog.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
}
/**
* Forces font color correction if the background color of the dialog box and the font color are too similar.
* It's a source configuration problem.
*/
textPaint.adjustTextColor(dialog, bitmap)
textPaint.color = Color.BLACK
textPaint.bgColor = Color.WHITE
return dialogBox
}
@ -241,59 +234,46 @@ class ComposedImageInterceptor(
setIncludePad(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
}
}.build()
}
// Invert color in black dialog box.
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {
val pixelColor = bitmap.getPixel(dialog.centerX.toInt(), dialog.centerY.toInt())
val inverseColor = (Color.WHITE - pixelColor) or Color.BLACK
val minDistance = 80f // arbitrary
if (colorDistance(pixelColor, dialog.foregroundColor) > minDistance) {
return
}
color = inverseColor
}
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
private fun Canvas.draw(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
save()
translate(x, y)
rotate(dialog.angle)
layout.draw(this)
drawTextOutline(textPaint, layout)
drawText(textPaint, layout)
restore()
}
private fun Canvas.drawText(textPaint: TextPaint, layout: StaticLayout) {
textPaint.style = Paint.Style.FILL
layout.draw(this)
}
private fun Canvas.drawTextOutline(textPaint: TextPaint, layout: StaticLayout) {
val foregroundColor = textPaint.color
val style = textPaint.style
textPaint.strokeWidth = 5F
textPaint.color = textPaint.bgColor
textPaint.style = Paint.Style.FILL_AND_STROKE
layout.draw(this)
textPaint.color = foregroundColor
textPaint.style = style
}
// https://pixelsconverter.com/pt-to-px
private val Int.pt: Float get() = this / SCALED_DENSITY
// ============================= Utils ======================================
/**
* Calculates the Euclidean distance between two colors in RGB space.
*
* This function takes two integer values representing hexadecimal colors,
* converts them to their RGB components, and calculates the Euclidean distance
* between the two colors. The distance provides a measure of how similar or
* different the two colors are.
*
*/
private fun colorDistance(colorA: Int, colorB: Int): Double {
val a = Color.valueOf(colorA)
val b = Color.valueOf(colorB)
return sqrt(
(b.red() - a.red()).toDouble().pow(2) +
(b.green() - a.green()).toDouble().pow(2) +
(b.blue() - a.blue()).toDouble().pow(2),
)
}
companion object {
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt

View File

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

View File

@ -82,7 +82,12 @@ abstract class Madara(
/**
* Automatically fetched genres from the source to be used in the filters.
*/
private var genresList: List<Genre> = emptyList()
protected open var genresList: List<Genre> = emptyList()
/**
* Whether genres have been fetched
*/
private var genresFetched: Boolean = false
/**
* Inner variable to control how much tries the genres request was called.
@ -237,11 +242,14 @@ abstract class Madara(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
return client.newCall(GET("$baseUrl$mangaUrl", headers))
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(mangaSubString)
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
}.build()
return client.newCall(GET(mangaUrl, headers))
.asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply {
url = mangaUrl
setUrlWithoutDomain(mangaUrl.toString())
}
MangasPage(listOf(manga), false)
@ -578,11 +586,13 @@ abstract class Madara(
override fun searchMangaSelector() = "div.c-tabs-item__content"
protected open val searchMangaUrlSelector = "div.post-title a"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
with(element) {
selectFirst("div.post-title a")!!.let {
selectFirst(searchMangaUrlSelector)!!.let {
manga.setUrlWithoutDomain(it.attr("abs:href"))
manga.title = it.ownText()
}
@ -623,7 +633,7 @@ abstract class Madara(
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
"Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso", "Atualizações Semanais",
)
protected val hiatusStatusList: Array<String> = arrayOf(
@ -678,7 +688,7 @@ abstract class Madara(
manga.thumbnail_url = imageFromElement(it)
}
select(mangaDetailsSelectorStatus).last()?.let {
manga.status = with(it.text()) {
manga.status = with(it.text().filter { ch -> ch.isLetterOrDigit() || ch.isWhitespace() }.trim()) {
when {
containsIn(completedStatusList) -> SManga.COMPLETED
containsIn(ongoingStatusList) -> SManga.ONGOING
@ -742,7 +752,7 @@ abstract class Madara(
// Manga Details Selector
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
open val mangaDetailsSelectorAuthor = "div.author-content > a"
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
open val mangaDetailsSelectorArtist = "div.artist-content > a"
open val mangaDetailsSelectorStatus = "div.summary-content"
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
@ -776,7 +786,7 @@ abstract class Madara(
/**
* Get the best image quality available from srcset
*/
private fun String.getSrcSetImage(): String? {
protected fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
@ -920,6 +930,10 @@ abstract class Madara(
WordSet("hace").startsWith(date) -> {
parseRelativeDate(date)
}
// Handle "jour" with a number before it
date.contains(Regex("""\b\d+ jour""")) -> {
parseRelativeDate(date)
}
date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
date.split(" ").map {
@ -1063,10 +1077,17 @@ abstract class Madara(
* Fetch the genres from the source to be used in the filters.
*/
protected fun fetchGenres() {
if (fetchGenres && fetchGenresAttempts < 3 && genresList.isEmpty()) {
if (fetchGenres && fetchGenresAttempts < 3 && !genresFetched) {
try {
genresList = client.newCall(genresRequest()).execute()
client.newCall(genresRequest()).execute()
.use { parseGenres(it.asJsoup()) }
.also {
genresFetched = true
}
.takeIf { it.isNotEmpty() }
?.also {
genresList = it
}
} catch (_: Exception) {
} finally {
fetchGenresAttempts++

View File

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

View File

@ -11,11 +11,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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@ -25,7 +22,6 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
@ -55,8 +51,6 @@ abstract class MadTheme(
add("Referer", "$baseUrl/")
}
private val json: Json by injectLazy()
private var genreKey = "genre[]"
// Popular
@ -177,19 +171,53 @@ abstract class MadTheme(
}
override fun chapterListParse(response: Response): List<SChapter> {
if (response.code in 200..299) {
if (response.request.url.fragment == "idFound") {
return super.chapterListParse(response)
}
// Try to show message/error from site
response.body.let { body ->
json.decodeFromString<JsonObject>(body.string())["message"]
?.jsonPrimitive
?.content
?.let { throw Exception(it) }
val document = response.asJsoup()
// Need the total chapters to check against the request
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
?.substringAfter("(")
?.substringBefore(")")
?.toIntOrNull()
val script = document.selectFirst("script:containsData(bookId)")
?: throw Exception("Cannot find script")
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
// Use slug search by default
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
if (!slugRequest.isSuccessful) {
throw Exception("HTTP error ${slugRequest.code}")
}
throw Exception("HTTP error ${response.code}")
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
if (totalChapters != null && finalDocument.size < totalChapters) {
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
finalDocument = idRequest.asJsoup().select(chapterListSelector())
}
return finalDocument.map {
SChapter.create().apply {
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
name = it.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
}
}
}
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
return baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addPathSegment("manga")
addPathSegment(fetchByParam)
addPathSegment("chapters")
addQueryParameter("source", "detail")
}.build()
}
override fun chapterListRequest(manga: SManga): Request =
@ -197,10 +225,11 @@ abstract class MadTheme(
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title)
.fragment("idFound")
.build()
GET(url, headers)
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
} ?: GET("$baseUrl${manga.url}", headers)
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {

View File

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

View File

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

View File

@ -1,129 +1,356 @@
package eu.kanade.tachiyomi.multisrc.mangareader
import android.app.Application
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import org.jsoup.nodes.TextNode
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
abstract class MangaReader : HttpSource(), ConfigurableSource {
abstract class MangaReader(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : HttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
private val json: Json by injectLazy()
open fun addPage(page: Int, builder: HttpUrl.Builder) {
builder.addQueryParameter("page", page.toString())
}
// ============================== Popular ===============================
protected open val sortPopularValue = "most-viewed"
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortPopularValue)),
)
}
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
final override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
val volume = SManga.create().apply {
url = manga.url + VOLUME_URL_SUFFIX
title = VOLUME_TITLE_PREFIX + manga.title
thumbnail_url = manga.thumbnail_url
// =============================== Latest ===============================
protected open val sortLatestValue = "latest-updated"
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortLatestValue)),
)
}
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
protected open val searchPathSegment = "search"
protected open val searchKeyword = "keyword"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment(searchPathSegment)
addQueryParameter(searchKeyword, query)
} else {
addPathSegment("filter")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
listOf(manga, volume)
}
addPage(page, this)
}.build()
return GET(url, headers)
}
open fun searchMangaSelector(): String = ".manga_list-sbs .manga-poster"
open fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("img")!!.let {
title = it.attr("alt")
thumbnail_url = it.imgAttr()
}
}
open fun searchMangaNextPageSelector(): String = "ul.pagination > li.active + li"
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(searchMangaSelector())
.map(::searchMangaFromElement)
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage)
}
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
// =========================== Manga Details ============================
abstract fun searchMangaSelector(): String
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
abstract fun searchMangaNextPageSelector(): String
private val authorText: String = when (lang) {
"ja" -> "著者"
else -> "Authors"
}
abstract fun searchMangaFromElement(element: Element): SManga
private val statusText: String = when (lang) {
"ja" -> "地位"
else -> "Status"
}
abstract fun mangaDetailsParse(document: Document): SManga
final override fun mangaDetailsParse(response: Response): SManga {
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
val manga = mangaDetailsParse(document)
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
manga.title = VOLUME_TITLE_PREFIX + manga.title
return SManga.create().apply {
document.selectFirst("#ani_detail")!!.run {
title = selectFirst(".manga-name")!!.ownText()
thumbnail_url = selectFirst("img")?.imgAttr()
genre = select(".genres > a").joinToString { it.ownText() }
description = buildString {
selectFirst(".description")?.ownText()?.let { append(it) }
append("\n\n")
selectFirst(".manga-name-or")?.ownText()?.let {
if (it.isNotEmpty() && it != title) {
append("Alternative Title: ")
append(it)
}
}
}.trim()
select(".anisc-info > .item").forEach { info ->
when (info.selectFirst(".item-head")?.ownText()) {
"$authorText:" -> info.parseAuthorsTo(this@apply)
"$statusText:" -> info.parseStatus(this@apply)
}
}
}
}
}
private fun Element.parseAuthorsTo(manga: SManga): SManga {
val authors = select("a")
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size
when (count) {
0 -> return manga
1 -> {
manga.author = text.first()
return manga
}
}
val authorList = ArrayList<String>(count)
val artistList = ArrayList<String>(count)
for ((index, author) in authors.withIndex()) {
val textNode = author.nextSibling() as? TextNode
val list = if (textNode?.wholeText?.contains("(Art)") == true) artistList else authorList
list.add(text[index])
}
if (authorList.isNotEmpty()) manga.author = authorList.joinToString()
if (artistList.isNotEmpty()) manga.artist = artistList.joinToString()
return manga
}
abstract val chapterType: String
abstract val volumeType: String
private fun Element.parseStatus(manga: SManga): SManga {
manga.status = this.selectFirst(".name")?.text().getStatus()
return manga
}
abstract fun chapterListRequest(mangaUrl: String, type: String): Request
open fun String?.getStatus(): Int = when (this?.lowercase()) {
"ongoing", "publishing", "releasing" -> SManga.ONGOING
"completed", "finished" -> SManga.COMPLETED
"on-hold", "on_hiatus" -> SManga.ON_HIATUS
"canceled", "discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
// ============================== Chapters ==============================
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url.substringBeforeLast('#')
}
open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit
open val chapterIdSelect = "en-chapters"
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val path = manga.url
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
val type = if (isVolume) volumeType else chapterType
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
val response = client.newCall(request).execute()
open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst("a")!!.run {
setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}")
name = selectFirst(".name")?.text() ?: text()
}
}
val abbrPrefix = if (isVolume) "Vol" else "Chap"
val fullPrefix = if (isVolume) "Volume" else "Chapter"
val linkSelector = Evaluator.Tag("a")
parseChapterElements(response, isVolume).map { element ->
SChapter.create().apply {
val number = element.attr("data-number")
chapter_number = number.toFloatOrNull() ?: -1f
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
}
val link = element.selectFirst(linkSelector)!!
name = run {
val name = link.text()
val prefix = "$abbrPrefix $number: "
if (!name.startsWith(prefix)) return@run name
val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
}
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
// =============================== Pages ================================
open fun getChapterId(chapter: SChapter): String {
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
return document.selectFirst("div[data-reading-id]")
?.attr("data-reading-id")
.orEmpty()
.ifEmpty {
throw Exception("Unable to retrieve chapter id")
}
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
}
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
open fun getAjaxUrl(id: String): String {
return "$baseUrl//ajax/image/list/$id?mode=vertical"
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_VOLUME_PREF
title = "Show volume entries in search result"
setDefaultValue(false)
}.let(screen::addPreference)
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
getChapterId(chapter)
}
val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET(getAjaxUrl(chapterId), ajaxHeaders)
}
companion object {
private const val SHOW_VOLUME_PREF = "show_volume"
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
private const val VOLUME_URL_FRAGMENT = "vol"
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
private const val VOLUME_TITLE_PREFIX = "[VOL] "
override fun pageListParse(response: Response): List<Page> {
val document = response.parseHtmlProperty()
val pageList = document.select(pageListParseSelector()).mapIndexed { index, element ->
val imgUrl = element.imgAttr().ifEmpty {
element.selectFirst("img")!!.imgAttr()
}
Page(index, imageUrl = imgUrl)
}
return pageList
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
open fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-url") -> attr("abs:data-url")
else -> attr("abs:src")
}
open fun Response.parseHtmlProperty(): Document {
val html = json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
// =============================== Filters ==============================
object Note : Filter.Header("NOTE: Ignored if using text search!")
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
private val join: String? = null,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (join == null) {
checked.forEach {
builder.addQueryParameter(param, it.value)
}
} else {
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
}
}
}
open class SortFilter(
title: String,
param: String,
values: Array<Pair<String, String>>,
default: String? = null,
) : UriPartFilter(title, param, values, default)
private val sortFilterName: String = when (lang) {
"ja" -> "選別"
else -> "Sort"
}
protected open val sortFilterParam: String = "sort"
protected open fun sortFilterValues(): Array<Pair<String, String>> {
return arrayOf(
Pair("Default", "default"),
Pair("Latest Updated", sortLatestValue),
Pair("Score", "score"),
Pair("Name A-Z", "name-az"),
Pair("Release Date", "release-date"),
Pair("Most Viewed", sortPopularValue),
)
}
open fun getSortFilter() = SortFilter(sortFilterName, sortFilterParam, sortFilterValues())
override fun getFilterList(): FilterList = FilterList(
getSortFilter(),
)
}

View File

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

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.multisrc.mangaworld
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieRegex = Regex("""document\.cookie="(MWCookie[^"]+)""")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// ignore requests that already have completed the JS challenge
if (response.headers["vary"] != null) return response
val content = response.body.string()
val results = cookieRegex.find(content)
?: return response.newBuilder().body(content.toResponseBody(response.body.contentType())).build()
val (cookieString) = results.destructured
return chain.proceed(loadCookie(request, cookieString))
}
private fun loadCookie(request: Request, cookieString: String): Request {
val cookie = Cookie.parse(request.url, cookieString)!!
client.cookieJar.saveFromResponse(request.url, listOf(cookie))
val headers = request.headers.newBuilder()
.add("Cookie", cookie.toString())
.build()
return GET(request.url, headers)
}
}

View File

@ -28,7 +28,11 @@ abstract class MangaWorld(
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// CookieRedirectInterceptor extracts MWCookie from the page's JS code, applies it and then redirects to the page
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(CookieRedirectInterceptor(network.cloudflareClient))
.build()
companion object {
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }

View File

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

View File

@ -267,7 +267,7 @@ abstract class SlimeReadTheme(
companion object {
const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -1,4 +1,5 @@
include(":core")
include(":utils")
// Load all modules under /lib
File(rootDir, "lib").eachDir { include("lib:${it.name}") }

View File

@ -14,8 +14,12 @@
<data android:host="*.bato.to" />
<data android:host="bato.to" />
<data android:host="*.batocc.com" />
<data android:host="batocc.com" />
<data android:host="*.batocomic.com" />
<data android:host="batocomic.com" />
<data android:host="*.batocomic.net" />
<data android:host="batocomic.net" />
<data android:host="*.batocomic.org" />
<data android:host="batocomic.org" />
<data android:host="*.batotoo.com" />
<data android:host="batotoo.com" />
<data android:host="*.batotwo.com" />
@ -24,18 +28,40 @@
<data android:host="battwo.com" />
<data android:host="*.comiko.net" />
<data android:host="comiko.net" />
<data android:host="*.comiko.org" />
<data android:host="comiko.org" />
<data android:host="*.mangatoto.com" />
<data android:host="mangatoto.com" />
<data android:host="*.mangatoto.net" />
<data android:host="mangatoto.net" />
<data android:host="*.mangatoto.org" />
<data android:host="mangatoto.org" />
<data android:host="*.mycordant.co.uk" />
<data android:host="mycordant.co.uk" />
<data android:host="*.readtoto.com" />
<data android:host="readtoto.com" />
<data android:host="*.readtoto.net" />
<data android:host="readtoto.net" />
<data android:host="*.readtoto.org" />
<data android:host="readtoto.org" />
<data android:host="*.xbato.com" />
<data android:host="xbato.com" />
<data android:host="*.xbato.net" />
<data android:host="xbato.net" />
<data android:host="*.xbato.org" />
<data android:host="xbato.org" />
<data android:host="*.zbato.com" />
<data android:host="zbato.com" />
<data android:host="*.zbato.net" />
<data android:host="zbato.net" />
<data android:host="*.zbato.org" />
<data android:host="zbato.org" />
<data android:host="*.dto.to" />
<data android:host="dto.to" />
<data android:host="*.fto.to" />
<data android:host="fto.to" />
<data android:host="*.hto.to" />
<data android:host="hto.to" />
<data android:host="*.jto.to" />
<data android:host="jto.to" />
<data android:host="*.mto.to" />
<data android:host="mto.to" />
<data android:host="*.wto.to" />

View File

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

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
import eu.kanade.tachiyomi.network.GET
@ -100,11 +101,25 @@ open class BatoTo(
if (current.isNotEmpty()) {
return current
}
field = getMirrorPref()!!
field = getMirrorPref()
return field
}
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
private fun getMirrorPref(): String {
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
?: let {
val seed = runCatching {
val pm = Injekt.get<Application>().packageManager
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
}.getOrElse {
BuildConfig.VERSION_NAME.hashCode().toLong()
}
MIRROR_PREF_ENTRY_VALUES[1 + (seed % (MIRROR_PREF_ENTRIES.size - 1)).toInt()]
}
}
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
private fun isRemoveTitleVersion(): Boolean {
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
@ -327,6 +342,12 @@ open class BatoTo(
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = manga.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
return GET(manga.url, headers)
}
return super.mangaDetailsRequest(manga)
@ -337,8 +358,8 @@ open class BatoTo(
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
val manga = SManga.create()
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
val workStatus = infoElement.selectFirst("div.attr-item:contains(original work) span")?.text()
val uploadStatus = infoElement.selectFirst("div.attr-item:contains(upload status) span")?.text()
val originalTitle = infoElement.select("h3").text().removeEntities()
val description = buildString {
append(infoElement.select("div.limit-html").text())
@ -346,13 +367,13 @@ open class BatoTo(
append("\n\n${it.text()}")
}
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
append("\n\nExtra Info:\n${it.text()}")
append("\n\nExtra Info:\n${it.wholeText()}")
}
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
document.selectFirst("div.pb-2.alias-set.line-b-f")?.takeIf { it.hasText() }?.also {
append("\n\nAlternative Titles:\n")
append(it.text().split('/').joinToString("\n") { "${it.trim()}" })
}
}
}.trim()
val cleanedTitle = if (isRemoveTitleVersion()) {
originalTitle.replace(titleRegex, "").trim()
@ -369,16 +390,19 @@ open class BatoTo(
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
return manga
}
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
workStatus == null -> SManga.UNKNOWN
workStatus.contains("Ongoing") -> SManga.ONGOING
workStatus.contains("Cancelled") -> SManga.CANCELLED
workStatus.contains("Hiatus") -> SManga.ON_HIATUS
workStatus.contains("Completed") -> when {
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
else -> SManga.COMPLETED
private fun parseStatus(workStatus: String?, uploadStatus: String?): Int {
val status = workStatus ?: uploadStatus
return when {
status == null -> SManga.UNKNOWN
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Cancelled") -> SManga.CANCELLED
status.contains("Hiatus") -> SManga.ON_HIATUS
status.contains("Completed") -> when {
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
else -> SManga.COMPLETED
}
else -> SManga.UNKNOWN
}
else -> SManga.UNKNOWN
}
private fun altChapterParse(response: Response): List<SChapter> {
@ -411,6 +435,12 @@ open class BatoTo(
GET("$baseUrl/rss/series/$id.xml", headers)
} else if (manga.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = manga.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
GET(manga.url, headers)
} else {
super.chapterListRequest(manga)
@ -507,6 +537,12 @@ open class BatoTo(
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = chapter.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
return GET(chapter.url, headers)
}
return super.pageListRequest(chapter)
@ -1001,7 +1037,7 @@ open class BatoTo(
private const val MIRROR_PREF_TITLE = "Mirror"
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
private val MIRROR_PREF_ENTRIES = arrayOf(
"zbato.org",
"Auto",
"batocomic.com",
"batocomic.net",
"batocomic.org",
@ -1013,23 +1049,25 @@ open class BatoTo(
"readtoto.com",
"readtoto.net",
"readtoto.org",
"dto.to",
"fto.to",
"jto.to",
"hto.to",
"mto.to",
"wto.to",
"xbato.com",
"xbato.net",
"xbato.org",
"zbato.com",
"zbato.net",
"zbato.org",
"dto.to",
"fto.to",
"hto.to",
"jto.to",
"mto.to",
"wto.to",
)
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
private val DEPRECATED_MIRRORS = listOf(
"https://bato.to",
"https://batocc.com", // parked
"https://mangatoto.com",
"https://mangatoto.net",
"https://mangatoto.org",

View File

@ -0,0 +1,9 @@
ext {
extName = 'Comic Growl'
extClass = '.ComicGrowl'
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Element
// TODO: get manga status
// TODO: filter by status
// TODO: change cdnUrl as a array(upstream)
class ComicGrowl : GigaViewer(
"コミックグロウル",
"https://comic-growl.com",
"all",
"https://cdn-img.comic-growl.com/public/page",
) {
override val publisher = "BUSHIROAD WORKS"
override val chapterListMode = CHAPTER_LIST_LOCKED
override val supportsLatest: Boolean = true
override val client: OkHttpClient =
super.client.newBuilder().addInterceptor(::imageIntercept).build()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// Show only ongoing works
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.select("h5").text()
thumbnail_url = element.select("div > img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun latestUpdatesSelector() =
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
title = element.select("div.data h3").text()
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun getCollections(): List<Collection> = listOf(
Collection("連載作品", ""),
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
return GET(url.build(), headers)
}
return GET(baseUrl, headers) // Currently just get all ongoing works
}
}

View File

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

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comico'
extClass = '.ComicoFactory'
extVersionCode = 5
extVersionCode = 6
isNsfw = true
}

View File

@ -24,6 +24,7 @@ import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Calendar
@ -159,7 +160,7 @@ open class Comico(
if (!chapter.name.endsWith(LOCK)) {
super.fetchPageList(chapter)
} else {
throw Error("You are not authorized to view this!")
throw Exception("You are not authorized to view this!")
}
private fun search(query: String, page: Int) =
@ -176,7 +177,7 @@ open class Comico(
private val Response.data: JsonElement?
get() = json.parseToJsonElement(body.string()).jsonObject.also {
val code = it["result"]["code"].jsonPrimitive.int
if (code != 200) throw Error(status(code))
if (code != 200) throw Exception(status(code))
}["data"]
private operator fun JsonElement?.get(key: String) =

View File

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

View File

@ -54,7 +54,7 @@ class CosplayTele : ParsedHttpSource() {
override fun latestUpdatesNextPageSelector() = ".next.page-number"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
override fun latestUpdatesSelector() = "div.box"
override fun latestUpdatesSelector() = "main div.box"
// Popular
override fun popularMangaFromElement(element: Element): SManga {

View File

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

View File

@ -1,6 +1,11 @@
package eu.kanade.tachiyomi.extension.all.deviantart
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -15,16 +20,22 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class DeviantArt : HttpSource() {
class DeviantArt : HttpSource(), ConfigurableSource {
override val name = "DeviantArt"
override val baseUrl = "https://deviantart.com"
override val baseUrl = "https://www.deviantart.com"
override val lang = "all"
override val supportsLatest = false
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
}
@ -76,28 +87,32 @@ class DeviantArt : HttpSource() {
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
val subFolderGallery = document.selectFirst("#sub-folder-gallery")
val manga = SManga.create().apply {
// If manga is sub-gallery then use sub-gallery name, else use gallery name
title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
val gallery = document.selectFirst("#sub-folder-gallery")
// If manga is sub-gallery then use sub-gallery name, else use gallery name
val galleryName = gallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
?: gallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
val artistInTitle = preferences.artistInTitle == ArtistInTitle.ALWAYS.name ||
preferences.artistInTitle == ArtistInTitle.ONLY_ALL_GALLERIES.name && galleryName == "All"
return SManga.create().apply {
setUrlWithoutDomain(response.request.url.toString())
author = document.title().substringBefore(" ")
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
title = when (artistInTitle) {
true -> "$author - $galleryName"
false -> galleryName
}
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
}
manga.setUrlWithoutDomain(response.request.url.toString())
return manga
}
override fun chapterListRequest(manga: SManga): Request {
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
val username = pathSegments[0]
val folderId = pathSegments[2]
val query = if (folderId == "all") {
"gallery:$username"
} else {
"gallery:$username/$folderId"
val query = when (val folderId = pathSegments[2]) {
"all" -> "gallery:$username"
else -> "gallery:$username/$folderId"
}
val url = backendBuilder()
@ -123,15 +138,14 @@ class DeviantArt : HttpSource() {
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
}
return indexChapterList(chapterList.toList())
return chapterList.toList().also(::indexChapterList)
}
private fun parseToChapterList(document: Document): List<SChapter> {
val items = document.select("item")
return items.map {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
chapter.apply {
SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text())
name = it.selectFirst("title")!!.text()
date_upload = parseDate(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text()
@ -139,24 +153,34 @@ class DeviantArt : HttpSource() {
}
}
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
private fun indexChapterList(chapterList: List<SChapter>) {
// DeviantArt allows users to arrange galleries arbitrarily so we will
// primitively index the list by checking the first and last dates
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.mapIndexed { i, chapter ->
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = chapterList.size - i.toFloat()
}
} else {
chapterList.mapIndexed { i, chapter ->
chapter.apply { chapter_number = i.toFloat() + 1 }
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = i.toFloat() + 1
}
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return listOf(Page(0, imageUrl = imageUrl))
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) {
null -> listOf(Page(0, imageUrl = firstImageUrl))
else -> buttons.mapIndexed { i, button ->
// Remove everything past "/v1/" to get original instead of thumbnail
val imageUrl = button.selectFirst("img")?.absUrl("src")?.substringBefore("/v1/")
Page(i, imageUrl = imageUrl)
}.also {
// First image needs token to get original, which is included in firstImageUrl
it[0].imageUrl = firstImageUrl
}
}
}
override fun imageUrlParse(response: Response): String {
@ -167,7 +191,38 @@ class DeviantArt : HttpSource() {
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val artistInTitlePref = ListPreference(screen.context).apply {
key = ArtistInTitle.PREF_KEY
title = "Artist name in manga title"
entries = ArtistInTitle.values().map { it.text }.toTypedArray()
entryValues = ArtistInTitle.values().map { it.name }.toTypedArray()
summary = "Current: %s\n\n" +
"Changing this preference will not automatically apply to manga in Library " +
"and History, so refresh all DeviantArt manga and/or clear database in Settings " +
"> Advanced after doing so."
setDefaultValue(ArtistInTitle.defaultValue.name)
}
screen.addPreference(artistInTitlePref)
}
private enum class ArtistInTitle(val text: String) {
NEVER("Never"),
ALWAYS("Always"),
ONLY_ALL_GALLERIES("Only in \"All\" galleries"),
;
companion object {
const val PREF_KEY = "artistInTitlePref"
val defaultValue = ONLY_ALL_GALLERIES
}
}
private val SharedPreferences.artistInTitle
get() = getString(ArtistInTitle.PREF_KEY, ArtistInTitle.defaultValue.name)
companion object {
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
private const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.all.eromanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Eromanhwa : Madara(
"Eromanhwa",
"https://eromanhwa.org",
"all",
) {
override val id = 3597355706480775153 // accidently set lang to en...
override val useNewChapterEndpoint = true
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,271 +0,0 @@
package eu.kanade.tachiyomi.extension.all.freleinbooks
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class FreleinBooks() : ParsedHttpSource() {
override val baseUrl = "https://books.frelein.my.id"
override val lang = "all"
override val name = "Frelein Books"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val Element.imgSrc: String
get() = attr("data-lazy-src")
.ifEmpty { attr("data-src") }
.ifEmpty { attr("src") }
// Latest
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select(".postTitle").text()
manga.setUrlWithoutDomain(element.select(".postTitle > a").attr("abs:href"))
return manga
}
override fun latestUpdatesNextPageSelector() = ".olderLink"
override fun latestUpdatesRequest(page: Int): Request {
return if (page == 1) {
GET(baseUrl)
} else {
val dateParam = page * 7 * 2
// Calendar set to the current date
val calendar: Calendar = Calendar.getInstance()
// rollback 14 days
calendar.add(Calendar.DAY_OF_YEAR, -dateParam)
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
// now the date is 14 days back
GET("$baseUrl/search?updated-max=${formatter.format(calendar.time)}T12:38:00%2B07:00&max-results=12&start=12&by-date=false")
}
}
override fun latestUpdatesSelector() = ".blogPosts > article"
// Popular
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select("h3").text()
manga.setUrlWithoutDomain(element.select("h3 > a").attr("abs:href"))
return manga
}
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
override fun popularMangaSelector() = ".itemPopulars article"
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val tagFilter = filterList.findInstance<TagFilter>()!!
val groupFilter = filterList.findInstance<GroupFilter>()!!
val magazineFilter = filterList.findInstance<MagazineFilter>()!!
val fashionMagazineFilter = filterList.findInstance<FashionMagazineFilter>()!!
return when {
query.isEmpty() && groupFilter.state != 0 -> GET("$baseUrl/search/label/${groupFilter.toUriPart()}")
query.isEmpty() && magazineFilter.state != 0 -> GET("$baseUrl/search/label/${magazineFilter.toUriPart()}")
query.isEmpty() && fashionMagazineFilter.state != 0 -> GET("$baseUrl/search/label/${fashionMagazineFilter.toUriPart()}")
query.isEmpty() && tagFilter.state.isNotEmpty() -> GET("$baseUrl/search/label/${tagFilter.state}")
query.isNotEmpty() -> GET("$baseUrl/search?q=$query")
else -> latestUpdatesRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".postTitle").text()
manga.description = "Read ${document.select(".postTitle").text()} \n \nNote: If you encounters error when opening the magazine, please press the WebView button then leave a comment on our web so we can update it soon."
manga.genre = document.select(".labelLink > a")
.joinToString(", ") { it.text() }
manga.status = SManga.COMPLETED
return manga
}
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href"))
chapter.name = "Gallery"
chapter.date_upload = getDate(element.select("link[rel=\"canonical\"]").attr("href"))
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("noscript").remove()
document.select(".gallerybox a > img").forEachIndexed { i, it ->
// format new img/b/
if (it.imgSrc.contains("img/b/")) {
if (it.imgSrc.contains("/w768-rw/")) {
val itUrl = it.imgSrc.replace("/w768-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
if (it.imgSrc.contains("/w480-rw/")) {
val itUrl = it.imgSrc.replace("/w480-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
}
// format new img/b/
else {
if (it.imgSrc.contains("=w768-rw")) {
val itUrl = it.imgSrc.replace("=w768-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else if (it.imgSrc.contains("=w480-rw")) {
val itUrl = it.imgSrc.replace("=w480-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else {
val itUrl = it.imgSrc
pages.add(Page(i, itUrl, itUrl))
}
}
}
return pages
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Only one filter will be applied!"),
Filter.Separator(),
GroupFilter(),
MagazineFilter(),
FashionMagazineFilter(),
TagFilter(),
)
open class UriPartFilter(
displayName: String,
private val valuePair: Array<Pair<String, String>>,
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
fun toUriPart() = valuePair[state].second
}
class MagazineFilter : UriPartFilter(
"Magazine",
arrayOf(
Pair("Any", ""),
Pair("B.L.T.", "B.L.T."),
Pair("BIG ONE GIRLS", "BIG ONE GIRLS"),
Pair("BOMB!", "BOMB!"),
Pair("BRODY", "BRODY"),
Pair("BUBKA", "BUBKA"),
Pair("ENTAME", "ENTAME"),
Pair("EX Taishu", "EX Taishu"),
Pair("FINEBOYS", "FINEBOYS"),
Pair("FLASH", "FLASH"),
Pair("Fine", "Fine"),
Pair("Friday", "Friday"),
Pair("HINA_SATSU", "HINA_SATSU"),
Pair("IDOL AND READ", "IDOL AND READ"),
Pair("Kadokawa Scene 07", "Kadokawa Scene 07"),
Pair("Monthly Basketball", "Monthly Basketball"),
Pair("Monthly Young Magazine", "Monthly Young Magazine"),
Pair("NOGI_SATSU", "NOGI_SATSU"),
Pair("Nylon Japan", "Nylon Japan"),
Pair("Platinum FLASH", "Platinum FLASH"),
Pair("Shonen Magazine", "Shonen Magazine"),
Pair("Shukan Post", "Shukan Post"),
Pair("TOKYO NEWS MOOK", "TOKYO NEWS MOOK"),
Pair("TV LIFE,Tarzan", "TV LIFE,Tarzan"),
Pair("Tokyo Calendar", "Tokyo Calendar"),
Pair("Top Yell NEO", "Top Yell NEO"),
Pair("UTB", "UTB"),
Pair("Weekly Playboy", "Weekly Playboy"),
Pair("Weekly SPA", "Weekly SPA"),
Pair("Weekly SPA!", "Weekly SPA!"),
Pair("Weekly Shonen Champion", "Weekly Shonen Champion"),
Pair("Weekly Shonen Magazine", "Weekly Shonen Magazine"),
Pair("Weekly Shonen Sunday", "Weekly Shonen Sunday"),
Pair("Weekly Shounen Magazine", "Weekly Shounen Magazine"),
Pair("Weekly The Television Plus", "Weekly The Television Plus"),
Pair("Weekly Zero Jump", "Weekly Zero Jump"),
Pair("Yanmaga Web", "Yanmaga Web"),
Pair("Young Animal", "Young Animal"),
Pair("Young Champion", "Young Champion"),
Pair("Young Gangan", "Young Gangan"),
Pair("Young Jump", "Young Jump"),
Pair("Young Magazine", "Young Magazine"),
Pair("blt graph.", "blt graph."),
Pair("mini", "mini"),
),
)
class FashionMagazineFilter : UriPartFilter(
"Fashion Magazine",
arrayOf(
Pair("Any", ""),
Pair("BAILA", "BAILA"),
Pair("Biteki", "Biteki"),
Pair("CLASSY", "CLASSY"),
Pair("CanCam", "CanCam"),
Pair("JJ", "JJ"),
Pair("LARME", "LARME"),
Pair("MARQUEE", "MARQUEE"),
Pair("Maquia", "Maquia"),
Pair("Men's non-no", "Men's non-no"),
Pair("More", "More"),
Pair("Oggi", "Oggi"),
Pair("Ray", "Ray"),
Pair("Seventeen", "Seventeen"),
Pair("Sweet", "Sweet"),
Pair("VOCE", "VOCE"),
Pair("ViVi", "ViVi"),
Pair("With", "With"),
Pair("aR", "aR"),
Pair("anan", "anan"),
Pair("bis", "bis"),
Pair("non-no", "non-no"),
),
)
class GroupFilter : UriPartFilter(
"Group",
arrayOf(
Pair("Any", ""),
Pair("Hinatazaka46", "Hinatazaka46"),
Pair("Nogizaka46", "Nogizaka46"),
Pair("Sakurazaka46", "Sakurazaka46"),
Pair("Keyakizaka46", "Keyakizaka46"),
),
)
class TagFilter : Filter.Text("Tag")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private fun getDate(str: String): Long {
val regex = "[0-9]{4}\\/[0-9]{2}\\/[0-9]{2}".toRegex()
val match = regex.find(str)
return runCatching { DATE_FORMAT.parse(match!!.value)?.time }.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy/MM/dd", Locale.US)
}
}
}

View File

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

View File

@ -64,8 +64,11 @@ class Hitomi(
private val json: Json by injectLazy()
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::jxlContentTypeInterceptor)
.addInterceptor(::updateImageUrlInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
@ -748,6 +751,25 @@ class Hitomi(
}
}
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
val (ext, hash) = match.destructured
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()

View File

@ -1,7 +1,7 @@
ext {
extName = 'Little Garden'
extClass = '.LittleGarden'
extVersionCode = 2
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View File

@ -31,7 +31,7 @@ class LittleGarden : ParsedHttpSource() {
private const val cdnUrl = "https://littlexgarden.com/static/images/webp/"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))")
private val oricolPageRegex = Regex("\\{colored:(?<colored>.*?(?=,)),original:(?<original>.*?(?=,))")
private val oricolPageRegex = Regex("\\{colored:(.*?(?=,)),original:(.*?(?=,))")
private val oriPageRegex = Regex("""original:"(.*?(?="))""")
}
@ -176,10 +176,10 @@ class LittleGarden : ParsedHttpSource() {
val engChaps: IntArray = intArrayOf(970, 987, 992)
if (document.selectFirst("div.manga-name")!!.text().trim() == "One Piece" && (engChaps.contains(chapNb) || chapNb > 1004)) { // Permits to get French pages rather than English pages for some chapters
oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it ->
if (it.groups["colored"]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site
pages.add(Page(i, "", cdnUrl + it.groups["colored"]?.value?.replace("\"", "") + ".webp"))
if (it.groups[1]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site
pages.add(Page(i, "", cdnUrl + it.groups[1]?.value?.replace("\"", "") + ".webp"))
} else {
pages.add(Page(i, "", cdnUrl + it.groups["original"]?.value?.replace("\"", "") + ".webp"))
pages.add(Page(i, "", cdnUrl + it.groups[2]?.value?.replace("\"", "") + ".webp"))
}
}
} else {

View File

@ -23,6 +23,9 @@ data_saver_summary=Enables smaller, more compressed images
excluded_tags_mode=Excluded tags mode
filter_original_languages=Filter original languages
filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
final_chapter=Final chapter:
final_chapter_in_description=Final chapter in description
final_chapter_in_description_summary=Include a manga's final chapter number at the end of its description
format=Format
format_adaptation=Adaptation
format_anthology=Anthology
@ -76,8 +79,8 @@ original_language=Original language
original_language_filter_chinese=%s (Manhua)
original_language_filter_japanese=%s (Manga)
original_language_filter_korean=%s (Manhwa)
prefer_title_in_extension_language=Use Alternate Titles
prefer_title_in_extension_language_summary=If there is an alternate title available which matches the extension language, it will be used
prefer_title_in_extension_language=Use alternative titles
prefer_title_in_extension_language_summary=If there is an alternative title available which matches the extension language, it will be used
publication_demographic=Publication demographic
publication_demographic_josei=Josei
publication_demographic_none=None

View File

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

View File

@ -143,6 +143,11 @@ object MDConstants {
return "${preferExtensionLangTitlePref}_$dexLang"
}
private const val finalChapterInDescPref = "finalChapterInDesc"
fun getFinalChapterInDescPrefKey(dexLang: String): String {
return "${finalChapterInDescPref}_$dexLang"
}
private const val tagGroupContent = "content"
private const val tagGroupFormat = "format"
private const val tagGroupGenre = "genre"

View File

@ -424,6 +424,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
preferences.coverQuality,
preferences.altTitlesInDesc,
preferences.preferExtensionLangTitle,
preferences.finalChapterInDesc,
)
}
@ -773,12 +774,28 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
}
val finalChapterInDescPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getFinalChapterInDescPrefKey(dexLang)
title = helper.intl["final_chapter_in_description"]
summary = helper.intl["final_chapter_in_description_summary"]
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), checkValue)
.commit()
}
}
screen.addPreference(coverQualityPref)
screen.addPreference(tryUsingFirstVolumeCoverPref)
screen.addPreference(dataSaverPref)
screen.addPreference(standardHttpsPortPref)
screen.addPreference(altTitlesInDescPref)
screen.addPreference(preferExtensionLangTitlePref)
screen.addPreference(finalChapterInDescPref)
screen.addPreference(contentRatingPref)
screen.addPreference(originalLanguagePref)
screen.addPreference(blockedGroupsPref)
@ -860,6 +877,9 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
private val SharedPreferences.preferExtensionLangTitle
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true)
private val SharedPreferences.finalChapterInDesc
get() = getBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), true)
/**
* Previous versions of the extension allowed invalid UUID values to be stored in the
* preferences. This method clear invalid UUIDs in case the user have updated from

View File

@ -156,7 +156,7 @@ class MangaDexHelper(lang: String) {
*/
private fun String.removeEntitiesAndMarkdown(): String {
return removeEntities()
.substringBefore("---")
.substringBefore("\n---")
.replace(markdownLinksRegex, "$1")
.replace(markdownItalicBoldRegex, "$1")
.replace(markdownItalicRegex, "$1")
@ -324,6 +324,7 @@ class MangaDexHelper(lang: String) {
coverSuffix: String?,
altTitlesInDesc: Boolean,
preferExtensionLangTitle: Boolean,
finalChapterInDesc: Boolean,
): SManga {
val attr = mangaDataDto.attributes!!
@ -365,9 +366,12 @@ class MangaDexHelper(lang: String) {
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
var desc = (attr.description[lang] ?: attr.description["en"])
// Build description
val desc = mutableListOf<String>()
(attr.description[lang] ?: attr.description["en"])
?.removeEntitiesAndMarkdown()
.orEmpty()
?.let { desc.add(it) }
if (altTitlesInDesc) {
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
@ -379,12 +383,24 @@ class MangaDexHelper(lang: String) {
if (altTitles.isNotEmpty()) {
val altTitlesDesc = altTitles
.joinToString("\n", "${intl["alternative_titles"]}\n") { "$it" }
desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
desc.add(altTitlesDesc.removeEntities())
}
}
if (finalChapterInDesc) {
val finalChapter = mutableListOf<String>()
attr.lastVolume?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Vol.$it") }
attr.lastChapter?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Ch.$it") }
if (finalChapter.isNotEmpty()) {
val finalChapterDesc = finalChapter
.joinToString(" ", "${intl["final_chapter"]}\n")
desc.add(finalChapterDesc.removeEntities())
}
}
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
description = desc
description = desc.joinToString("\n\n")
author = authors.joinToString()
artist = artists.joinToString()
status = getPublicationStatus(attr, chapters)

View File

@ -1,9 +1,7 @@
ext {
extName = 'MangaFire'
extClass = '.MangaFireFactory'
themePkg = 'mangareader'
baseUrl = 'https://mangafire.to'
overrideVersionCode = 5
extVersionCode = 10
isNsfw = true
}

View File

@ -1,166 +1,190 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
constructor(name: String) : this(name, name)
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
sealed class Group(
open class UriPartFilter(
name: String,
val param: String,
values: List<Entry>,
) : Filter.Group<Entry>(name, values)
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
sealed class Select(
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
val param: String,
private val valuesMap: Map<String, String>,
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
open val selection: String
get() = valuesMap[values[state]]!!
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
}
class TypeFilter : Group("Type", "type[]", types)
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
private val types: List<Entry>
get() = listOf(
Entry("Manga", "manga"),
Entry("One-Shot", "one_shot"),
Entry("Doujinshi", "doujinshi"),
Entry("Light-Novel", "light_novel"),
Entry("Novel", "novel"),
Entry("Manhwa", "manhwa"),
Entry("Manhua", "manhua"),
)
class Genre(name: String, val id: String) : Filter.TriState(name) {
val selection: String
get() = (if (isExcluded()) "-" else "") + id
open class UriTriSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
state.forEach { s ->
when (s.state) {
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
}
}
}
}
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
val param = "genre[]"
class TypeFilter : UriMultiSelectFilter(
"Type",
"type",
arrayOf(
Pair("Manga", "manga"),
Pair("One-Shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Novel", "novel"),
Pair("Manhwa", "manhwa"),
Pair("Manhua", "manhua"),
),
)
val combineMode: Boolean
get() = state.filter { !it.isIgnored() }.size > 1
class GenreFilter : UriTriSelectFilter(
"Genres",
"genre[]",
arrayOf(
Pair("Action", "1"),
Pair("Adventure", "78"),
Pair("Avant Garde", "3"),
Pair("Boys Love", "4"),
Pair("Comedy", "5"),
Pair("Demons", "77"),
Pair("Drama", "6"),
Pair("Ecchi", "7"),
Pair("Fantasy", "79"),
Pair("Girls Love", "9"),
Pair("Gourmet", "10"),
Pair("Harem", "11"),
Pair("Horror", "530"),
Pair("Isekai", "13"),
Pair("Iyashikei", "531"),
Pair("Josei", "15"),
Pair("Kids", "532"),
Pair("Magic", "539"),
Pair("Mahou Shoujo", "533"),
Pair("Martial Arts", "534"),
Pair("Mecha", "19"),
Pair("Military", "535"),
Pair("Music", "21"),
Pair("Mystery", "22"),
Pair("Parody", "23"),
Pair("Psychological", "536"),
Pair("Reverse Harem", "25"),
Pair("Romance", "26"),
Pair("School", "73"),
Pair("Sci-Fi", "28"),
Pair("Seinen", "537"),
Pair("Shoujo", "30"),
Pair("Shounen", "31"),
Pair("Slice of Life", "538"),
Pair("Space", "33"),
Pair("Sports", "34"),
Pair("Super Power", "75"),
Pair("Supernatural", "76"),
Pair("Suspense", "37"),
Pair("Thriller", "38"),
Pair("Vampire", "39"),
),
)
class GenreModeFilter : Filter.CheckBox("Must have all the selected genres"), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
if (state) {
builder.addQueryParameter("genre_mode", "and")
}
}
}
private val genres: List<Genre>
get() = listOf(
Genre("Action", "1"),
Genre("Adventure", "78"),
Genre("Avant Garde", "3"),
Genre("Boys Love", "4"),
Genre("Comedy", "5"),
Genre("Demons", "77"),
Genre("Drama", "6"),
Genre("Ecchi", "7"),
Genre("Fantasy", "79"),
Genre("Girls Love", "9"),
Genre("Gourmet", "10"),
Genre("Harem", "11"),
Genre("Horror", "530"),
Genre("Isekai", "13"),
Genre("Iyashikei", "531"),
Genre("Josei", "15"),
Genre("Kids", "532"),
Genre("Magic", "539"),
Genre("Mahou Shoujo", "533"),
Genre("Martial Arts", "534"),
Genre("Mecha", "19"),
Genre("Military", "535"),
Genre("Music", "21"),
Genre("Mystery", "22"),
Genre("Parody", "23"),
Genre("Psychological", "536"),
Genre("Reverse Harem", "25"),
Genre("Romance", "26"),
Genre("School", "73"),
Genre("Sci-Fi", "28"),
Genre("Seinen", "537"),
Genre("Shoujo", "30"),
Genre("Shounen", "31"),
Genre("Slice of Life", "538"),
Genre("Space", "33"),
Genre("Sports", "34"),
Genre("Super Power", "75"),
Genre("Supernatural", "76"),
Genre("Suspense", "37"),
Genre("Thriller", "38"),
Genre("Vampire", "39"),
)
class StatusFilter : UriMultiSelectFilter(
"Status",
"status[]",
arrayOf(
Pair("Completed", "completed"),
Pair("Releasing", "releasing"),
Pair("On Hiatus", "on_hiatus"),
Pair("Discontinued", "discontinued"),
Pair("Not Yet Published", "info"),
),
)
class StatusFilter : Group("Status", "status[]", statuses)
class YearFilter : UriMultiSelectFilter(
"Year",
"year[]",
years,
) {
companion object {
private val currentYear by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
private val statuses: List<Entry>
get() = listOf(
Entry("Completed", "completed"),
Entry("Releasing", "releasing"),
Entry("On Hiatus", "on_hiatus"),
Entry("Discontinued", "discontinued"),
Entry("Not Yet Published", "info"),
)
private val years: Array<Pair<String, String>> = buildList(29) {
addAll(
(currentYear downTo (currentYear - 20)).map(Int::toString),
)
class YearFilter : Group("Year", "year[]", years)
addAll(
(2000 downTo 1930 step 10).map { "${it}s" },
)
}.map { Pair(it, it) }.toTypedArray()
}
}
private val years: List<Entry>
get() = listOf(
Entry("2023"),
Entry("2022"),
Entry("2021"),
Entry("2020"),
Entry("2019"),
Entry("2018"),
Entry("2017"),
Entry("2016"),
Entry("2015"),
Entry("2014"),
Entry("2013"),
Entry("2012"),
Entry("2011"),
Entry("2010"),
Entry("2009"),
Entry("2008"),
Entry("2007"),
Entry("2006"),
Entry("2005"),
Entry("2004"),
Entry("2003"),
Entry("2000s"),
Entry("1990s"),
Entry("1980s"),
Entry("1970s"),
Entry("1960s"),
Entry("1950s"),
Entry("1940s"),
)
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
if (state.isNotEmpty()) {
val value = state.toIntOrNull()?.takeIf { it > 0 }
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0")
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
builder.addQueryParameter("minchap", value.toString())
}
}
}
private val chapterCounts
get() = mapOf(
"Any" to "",
"At least 1 chapter" to "1",
"At least 3 chapters" to "3",
"At least 5 chapters" to "5",
"At least 10 chapters" to "10",
"At least 20 chapters" to "20",
"At least 30 chapters" to "30",
"At least 50 chapters" to "50",
)
class SortFilter : Select("Sort", "sort", orders)
private val orders
get() = mapOf(
"Trending" to "trending",
"Recently updated" to "recently_updated",
"Recently added" to "recently_added",
"Release date" to "release_date",
"Name A-Z" to "title_az",
"Score" to "scores",
"MAL score" to "mal_scores",
"Most viewed" to "most_viewed",
"Most favourited" to "most_favourited",
)
class SortFilter(defaultValue: String? = null) : UriPartFilter(
"Sort",
"sort",
arrayOf(
Pair("Most relevance", "most_relevance"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
Pair("Trending", "trending"),
Pair("Name A-Z", "title_az"),
Pair("Scores", "scores"),
Pair("MAL scores", "mal_scores"),
Pair("Most viewed", "most_viewed"),
Pair("Most favourited", "most_favourited"),
),
defaultValue,
)

View File

@ -1,12 +1,17 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import android.app.Application
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@ -18,182 +23,245 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
open class MangaFire(
class MangaFire(
override val lang: String,
private val langCode: String = lang,
) : MangaReader() {
) : ConfigurableSource, HttpSource() {
override val name = "MangaFire"
override val baseUrl = "https://mangafire.to"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor)
.build()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "most_viewed")),
)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "recently_updated")),
)
}
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
urlBuilder.addPathSegment("filter").apply {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("filter")
if (query.isNotBlank()) {
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
}
} else {
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("language[]", langCode)
addQueryParameter("page", page.toString())
filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
is Group -> {
filter.state.forEach {
if (it.state) {
addQueryParameter(filter.param, it.id)
}
}
}
is Select -> {
addQueryParameter(filter.param, filter.selection)
}
is GenresFilter -> {
filter.state.forEach {
if (it.state != 0) {
addQueryParameter(filter.param, it.selection)
}
}
if (filter.combineMode) {
addQueryParameter("genre_mode", "and")
}
}
else -> {}
}
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("language[]", langCode)
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
val volume = SManga.create().apply {
url = manga.url + VOLUME_URL_SUFFIX
title = VOLUME_TITLE_PREFIX + manga.title
thumbnail_url = manga.thumbnail_url
}
listOf(manga, volume)
}
}
return GET(urlBuilder.build(), headers)
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage)
}
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
override fun searchMangaSelector() = ".original.card-lg .unit .inner"
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
override fun searchMangaFromElement(element: Element) =
SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
element.selectFirst(Evaluator.Tag("img"))!!.let {
thumbnail_url = it.attr("src")
}
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(".info")!!
val mangaTitle = root.child(1).ownText()
title = mangaTitle
description = document.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = root.child(2).ownText()) {
"", mangaTitle -> description
else -> "$description\n\nAlternative Title: $altTitle"
// =============================== Filters ==============================
override fun getFilterList() = FilterList(
TypeFilter(),
GenreFilter(),
GenreModeFilter(),
StatusFilter(),
YearFilter(),
MinChapterFilter(),
SortFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup()).apply {
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
title = VOLUME_TITLE_PREFIX + title
}
}
thumbnail_url = document.selectFirst(".poster")!!
.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING
"On_hiatus" -> SManga.ON_HIATUS
"Discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
with(document.selectFirst(Evaluator.Class("meta"))!!) {
author = selectFirst("span:contains(Author:) + span")?.text()
val type = selectFirst("span:contains(Type:) + span")?.text()
val genres = selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
}
override val chapterType get() = "chapter"
override val volumeType get() = "volume"
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
title = selectFirst("h1")!!.text()
thumbnail_url = selectFirst(".poster img")?.attr("src")
status = selectFirst(".info > p").parseStatus()
description = buildString {
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
append(it.joinToString("\n\n"))
}
override fun chapterListRequest(mangaUrl: String, type: String): Request {
val id = mangaUrl.substringAfterLast('.')
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
}
selectFirst("h6")?.let {
append("\n\nAlternative title: ${it.text()}")
}
}.trim()
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val selector = if (isVolume) "div.unit" else "ul li"
val elements = document.select(selector)
if (elements.size > 0) {
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
val chapters = chapterInfoDocument.select("ul li")
for ((i, it) in elements.withIndex()) {
it.attr("data-id", chapters[i].select("a").attr("data-id"))
selectFirst(".meta")?.let {
author = it.selectFirst("span:contains(Author:) + span")?.text()
val type = it.selectFirst("span:contains(Type:) + span")?.text()
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
}
return elements.toList()
}
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"releasing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on_hiatus" -> SManga.ON_HIATUS
"discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url.substringBeforeLast("#")
}
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request {
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers)
}
@Serializable
class ChapterIdsDto(
class AjaxReadDto(
val html: String,
val title_format: String,
)
override fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
val request = chapterListRequest(manga.url, chapterType)
val response = client.newCall(request).execute()
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException()
}
val elements = document.selectFirst(".scroll-sm")!!.children()
val chapterCount = chapters.size
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
for (i in 0 until chapterCount) {
val chapter = chapters[i]
val element = elements[i]
val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
val date = element.select(Evaluator.Tag("span"))[1].ownText()
chapter.date_upload = try {
dateFormat.parse(date)!!.time
} catch (_: Throwable) {
0
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val path = manga.url
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
val type = if (isVolume) "volume" else "chapter"
val abbrPrefix = if (isVolume) "Vol" else "Chap"
val fullPrefix = if (isVolume) "Volume" else "Chapter"
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type))
.execute().parseAs<ResponseDto<String>>().result
.toBodyFragment()
.select(if (isVolume) ".vol-list > .item" else "li")
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type))
.execute().parseAs<ResponseDto<AjaxReadDto>>().result.html
.toBodyFragment()
.select("ul a")
val chapterList = ajaxMangaList.zip(ajaxReadList) { m, r ->
val link = r.selectFirst("a")!!
if (!r.attr("abs:href").toHttpUrl().pathSegments.last().contains(type)) {
return Observable.just(emptyList())
}
assert(m.attr("data-number") == r.attr("data-number")) {
"Chapter count doesn't match. Try updating again."
}
val number = m.attr("data-number")
val dateStr = m.select("span").getOrNull(1)?.text() ?: ""
SChapter.create().apply {
setUrlWithoutDomain("${link.attr("href")}#$type/${r.attr("data-id")}")
chapter_number = number.toFloatOrNull() ?: -1f
name = run {
val name = link.text()
val prefix = "$abbrPrefix $number: "
if (!name.startsWith(prefix)) return@run name
val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
}
date_upload = try {
dateFormat.parse(dateStr)!!.time
} catch (_: ParseException) {
0L
}
}
}
return Observable.just(chapterList)
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#')
return GET("$baseUrl/ajax/read/$typeAndId", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
val result = response.parseAs<ResponseDto<PageListDto>>().result
return result.pages.mapIndexed { index, image ->
val url = image.url
@ -206,27 +274,49 @@ open class MangaFire(
@Serializable
class PageListDto(private val images: List<List<JsonPrimitive>>) {
val pages get() = images.map {
Image(it[0].content, it[2].int)
}
val pages
get() = images.map {
Image(it[0].content, it[2].int)
}
}
class Image(val url: String, val offset: Int)
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_VOLUME_PREF
title = "Show volume entries in search result"
setDefaultValue(false)
}.let(screen::addPreference)
}
// ============================= Utilities ==============================
@Serializable
class ResponseDto<T>(
val result: T,
val status: Int,
)
override fun getFilterList() =
FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TypeFilter(),
GenresFilter(),
StatusFilter(),
YearFilter(),
ChapterCountFilter(),
SortFilter(),
)
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun String.toBodyFragment(): Document {
return Jsoup.parseBodyFragment(this, baseUrl)
}
companion object {
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
private const val SHOW_VOLUME_PREF = "show_volume"
private const val VOLUME_URL_FRAGMENT = "vol"
private const val VOLUME_URL_SUFFIX = "#$VOLUME_URL_FRAGMENT"
private const val VOLUME_TITLE_PREFIX = "[VOL] "
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.mangahosted.MangaHostedUrlActivity"
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"
android:host="mangahosted.org"
android:pathPattern="/.*/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,6 +1,6 @@
ext {
extName = 'Manga Latino'
extClass = '.MangaLatino'
extName = 'Manga Hosted'
extClass = '.MangaHostedFactory'
extVersionCode = 1
isNsfw = true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,202 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangahosted.org"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
// ================================= Popular ==========================================
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
// ================================= Latest ===========================================
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// ================================= Search ===========================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/SeachPage/$maxResult/${page - 1}".toHttpUrl().newBuilder()
.addPathSegment(query)
.build()
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
MangasPage(mangas, false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
return MangasPage(
dto.mangas.map(::mangaParse),
false,
)
}
// ================================= Details ==========================================
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
// ================================= Chapter ==========================================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = mutableListOf<SChapter>()
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage++)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
} while (chaptersDto.hasNextPage())
return Observable.just(chapters)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
val maxResult = 100
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/${langOption.orderBy}"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
// ================================= Pages ============================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = chapter.url.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun imageRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.removeAll("Referer")
.build()
return super.imageRequest(page).newBuilder()
.headers(imageHeaders)
.build()
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun imageUrlParse(response: Response): String = ""
// ================================= Utilities =======================================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun SManga.slug() = this.url.split("/").last()
private fun mangaParse(dto: MangaDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}
}

View File

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MangaDetailsDto(private val data: Props) {
val details: MangaDto get() = data.details
@Serializable
class Props(
@SerialName("infoDoc") val details: MangaDto,
)
}
@Serializable
open class Pageable<T>(
var currentPage: Int,
var totalPage: Int,
val data: List<T>,
) {
fun hasNextPage() = (currentPage + 1) <= totalPage
}
@Serializable
class ChapterDto(
val date: String,
@SerialName("idDoc") val slugManga: String,
@SerialName("idDetail") val id: String,
@SerialName("nameChapter") val name: String,
) {
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
}
@Serializable
class MangaDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("genresName") val genres: String,
@SerialName("status") val _status: String,
) {
val thumbnailUrl get() = "${MangaHosted.baseApiUrl}$_thumbnailUrl"
val status get() = when (_status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class SearchDto(
@SerialName("data")
val mangas: List<MangaDto>,
)
@Serializable
class PageDto(val `data`: Data) {
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
@Serializable
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
@Serializable
class DetailDocuments(val source: String)
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaHostedFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { MangaHosted(it) }
}
class LanguageOption(
val lang: String,
val infix: String = lang,
val mangaSubstring: String = infix,
val nameSuffix: String = "",
val orderBy: String = "DESC",
)
val languages = listOf(
LanguageOption("en", "manga", "scan"),
LanguageOption("en", "manga-v2", "kaka", " v2"),
LanguageOption("en", "comic", "comic-dc", " Comics"),
LanguageOption("es", "manga-spanish", "manga-es"),
LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
package eu.kanade.tachiyomi.extension.all.mangahosted
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,28 +7,30 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class AnimeHUrlActivity : Activity() {
class MangaHostedUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
val intent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id")
putExtra("query", slug(pathSegments))
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.e("AnimeHUrlActivity", e.toString())
Log.e("UnionMangasUrlActivity", e.toString())
}
} else {
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
private fun slug(pathSegments: List<String>) =
"${MangaHosted.SEARCH_PREFIX}${pathSegments[1]}"
}

View File

@ -1,3 +1,7 @@
## 1.4.7
- Reworked the lib-multisrc theme
## 1.3.4
- Refactor and make multisrc

View File

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

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