Compare commits

..

171 Commits

Author SHA1 Message Date
Vetle Ledaal 988d1b04af
Bato.to description: add extra info, format alternative titles (#7171)
CI / Prepare job (push) Successful in 4s Details
CI / Build individual modules (push) Successful in 7m23s Details
CI / Publish repo (push) Successful in 44s Details
* Bato.to: add extra info in description

* Bato.to: format alt titles
2025-01-18 20:14:01 +00:00
Fioren 924e064e42
Update TopTruyen & DocTruyen3Q (#7193)
update

- Update domain TopTruyen & DocTruyen3Q
- Fix timezone Toptruyen
- Fix blank thumbnail Doctruyen3Q
2025-01-18 20:14:01 +00:00
are-are-are 3766a24892
LxHentai update domain (#7195) 2025-01-18 20:14:01 +00:00
Chopper 554872b754
SussyToons: Fixes (#7211)
* Fix open http connection, dto serialization and loading chapter and pages

* Change message

* Save the last url of the chapter script

* Remove unused class

* Remove duplicate code

* Fix typo
2025-01-18 20:14:01 +00:00
dngonz 1b3368110f
Update domains (#7189)
update domains
2025-01-18 20:14:01 +00:00
Altometer 23d3d70961
Fixed image loading in HoneyManga (#7186) 2025-01-18 20:14:01 +00:00
Chopper 15f07bd2bd
Siikomik: Fix page loading (#7174)
Fix page loading
2025-01-18 20:14:01 +00:00
Hellkaros 87c6e30551
Add MysticMoon (#7169) 2025-01-18 20:14:01 +00:00
Chopper 7ae3695832
Snowmtl: Fix json serializer (#7168)
* Fix json serializer

* Fix lint
2025-01-18 20:14:01 +00:00
dngonz 03accf8717
WeebCentral: Exclude special characters (#7164)
exclude special characters for search
2025-01-18 20:14:01 +00:00
are-are-are c9af13410f
Update some domain (#7137)
* VlogTruyen update domain

* TruyenVN update domain

* Yurineko Update domain and add override URL

* NetTruyenCO update domain

* Fecomic update domain

* DuaLeoTruyen Update domain

* CoManhua Update domain

* HentaiVNPlus update domain

* Update src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNeko.kt

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:14:01 +00:00
Chopper 5687f616b2
SussyToons: Fix page loading (#7176)
Fix page loading
2025-01-18 20:14:01 +00:00
Tim Schneeberger 4ceef9ca9b
NamiComi: lower chapter list page size and fix missing tags (#7145)
* fix(namicomi): request tags in searchMangaRequest

* fix(namicomi): request chapter lists & chapter access maps in chunks of 200 entities

* fix(namicomi): read id directly from URL query parameter instead of using string manipulation

* chore(namicomi): bump version code

* Fix regression caused by simplification of createManga(...)

* Add missing slash

* Use new extension function to add parameters
2025-01-18 20:14:01 +00:00
Chopper cf29bdafd3
Add ApenasUmaFa (#7147)
* Add ApenasUmaFa

* Removes the super statement

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:14:01 +00:00
Hellkaros e7549fd4c1
HolyManga: Fix chapter title (#7166)
fix: chapter title
2025-01-18 20:14:00 +00:00
Hellkaros 9f62ebd9a3
Remove NetComics (#7167) 2025-01-18 20:14:00 +00:00
dngonz e12546007e
Kuroi manga: Fix chapter name (#7156)
* fix chapters name

* clean fun
2025-01-18 20:14:00 +00:00
dngonz 80aa0347fe
Jiangzaitoon: update domain (#7155)
update domain
2025-01-18 20:13:58 +00:00
dngonz 39cd7eb03f
Remove MyToon (#7154)
remove
2025-01-18 20:13:58 +00:00
dngonz 8dfcf0cea7
Manhwalist: Fix domain (#7153)
* fix domain

* fix gradle
2025-01-18 20:13:58 +00:00
Chopper 49efe333db
SlimeReadTheme: Fix pagination and add scan id (#7152)
Fix pagination and add scan id
2025-01-18 20:13:58 +00:00
Hellkaros d3d31ec815
FlameComics: Conditional adjustment of titles (#7150)
* fix: Conditional adjustment of titles

* fix: trailling comma
2025-01-18 20:13:58 +00:00
Aurel e86d2a850b
Fix: Improved date parsing for Keyoapp sites (#7140)
* Fix: Improve date parsing for ReaperScans, StarboundScans, and other Keyoapp sites & fix wrong dates time

* Update version codes

* Update lib-multisrc/keyoapp/src/eu/kanade/tachiyomi/multisrc/keyoapp/Keyoapp.kt

CamelCase

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

* Fix: Downgrade override version codes for ReaperScans and StarboundScans

* Fix: Standardize date selector variable naming

* Fix: Correct date format in AstralManga extension

* Fix: enhance relative date parsing & add support for french

* Revert Madara file for another PR and update astralmanga version

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:58 +00:00
Fathul Hidayat 8d8d46aad1
Komikcast: Change Domain (#7127) (#7136)
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:58 +00:00
Jefferson Abreu Martinez 34cd0bafcd
Fenix Manhwas: New domain URL (#7079)
* Fênix Manhwas domain and theme updated

* useNewChapterEndpoint true added

* versionId  added

zeistmanga -> madara
2025-01-18 20:13:58 +00:00
Hellkaros b088ba606b
HolyManga: Update theme to FMReader (#7131)
* fix: update theme to FMReader

* fix: update versionId
2025-01-18 20:13:54 +00:00
Hellkaros f5708a63fc
Add Novato Scans (#7128) 2025-01-18 20:13:54 +00:00
Hellkaros 32cea00999
Add Asmodeus Scans && Add La Zona del Lirio (#7123)
* Add Asmodeus Scans && Add La Zona del Lirio

* fix: add extra line
2025-01-18 20:13:54 +00:00
Aurel 079efdfea2
Fix: PhenixScans - Correctly parse chapter dates (#7119)
* Fix: PhenixScans: Handle incorrect chapter dates and future dates

* Bump overrideVersionCode to 2 in PhenixScans build.gradle

* Fix PhenixScans: Remove unused chapter parsing logic

* Fix PhenixScans: Remove unused imports
2025-01-18 20:13:54 +00:00
rebel onion 5ac0b9b5f6
e-hentai: favorite filtering and manual igneous cookie setting (#7098)
* feat: igneous header override

* feat: favorites filter

* chore: simple code clean

* chore: version bump

* feat: force e-hentai setting

* fix: requested changes

* fix: force default to true
2025-01-18 20:13:54 +00:00
Tim Schneeberger d470490087
Add NamiComi (#7057)
* Add Namicomi

* Remove conditional; already handled by the intent-filter

* Simplify error handling

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

* Update old comment

* Remove hardcoded /en/ url path

* Chapters missing from the accessMap should be considered inaccessible

* Remove setOnPreferenceChangeListener

* Remove unused i18n key

* Rename Namicomi to NamiComi

* Revert accidental change to settings.gradle.kts

* Remove remaining setOnPreferenceListener

* Close response on error

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

* Require nonnull chapter access map

* Change abstract to sealed in EntityDto

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Change abstract to sealed in MangaDto

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Change SManga.PUBLISHING_FINISHED to SManga.COMPLETED

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Set initialized = true

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Remove allowSpecialFloatingPointValues and prettyPrint

* Remove markdown cleanup functions

* Cleanup import

* Remove constructors for new sealed interface

* Fix PaginatedResponseDto structure

* Simplify and remove createBasicManga

* Remove old MangaDex code

* Use 🔒 for locked chapters

* Update NamiComiHelper.kt

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Remove data modifier from dto classes

* Apply suggestions from code review

Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>

* Update URL/ID handling

* Move companion object to bottom

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
2025-01-18 20:13:54 +00:00
AwkwardPeak7 a837998ad8
remove mangalife & mangasee 2025-01-18 20:13:54 +00:00
mrtear 82ac20e710
Add CasaComic (#7116)
* Add CasaComic

* small mistake

* fix

* Update src/en/casacomic/build.gradle

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:08 +00:00
Chopper 4f0481f71d
SussyToons: Fix loading pages (#7093)
* Fix loading pages

* Remove 'named capture groups' and add configuration

* Fix

* Use dynamic api url
2025-01-18 20:13:08 +00:00
Chopper 699740accb
ReadMangas: Update pathsegment (#7113)
Update pathsegment
2025-01-18 20:13:08 +00:00
Smol Ame f1428f8938
Remove Visormonarca (#7109) 2025-01-18 20:13:08 +00:00
Smol Ame 007b3f32bc
ReadTokyoGhoulRe&TokyoGhoulMangaOnline: Bump baseUrl & include extra series (#7108)
* ReadTokyoGhoul: Bump versionCode

* ReadTokyoGhoul: Bump baseUrl

* ReadTokyoGhoul: Add Choujin X
2025-01-18 20:13:08 +00:00
bapeey bdadddf27c
IkigaiMangas: Update cached value on preference change (#7089)
update cached value on pref update
2025-01-18 20:13:08 +00:00
Chopper 8d91fc6551
Add TemakiMangas (#7087) 2025-01-18 20:13:08 +00:00
Jefferson Abreu Martinez 3bea2c543e
Source request: Manhuanext (#7080)
* Source request: Manhuanext

* icon updated

* useNewChapterEndpoint true added
2025-01-18 20:13:08 +00:00
dngonz 91b1c3e65e
Keyoapp: extract manga details selectors (#7076)
* extract manga details selectors

* no bump
2025-01-18 20:13:08 +00:00
Chopper 038e9dcdcb
Toomics: Fix content loading (#7004)
* Fix selectors

* Add isNsfw

* Remove interpectors and use HttpUrl

* Fix lint

* Move code from fetchSearchManga to searchMangaParse
2025-01-18 20:13:08 +00:00
Chopper 580b2b1b16
Move snowmlt to lib-multisrc and add Solarmtl (#7054)
* Move snowmlt to lib-multisrc

* Fix snowmtl

* Remove assets from src
2025-01-18 20:13:05 +00:00
dngonz 690c553b6c
Add ApeComics (#7083) 2025-01-18 20:13:05 +00:00
dngonz e1c5cc473e
MyReadingManga: fix split filter check (#7073)
fix
2025-01-18 20:13:05 +00:00
dngonz c9b85253ed
Manhwaxxl: Fix source (#7066)
* fix

* fix comment

* fix comment

* add all genre and remove import
2025-01-18 20:13:05 +00:00
Dr1ks 677d9d17c0
Mintmanga: Fix image loading (#7030)
* Mintmanga: Fix image loading

* Mintmanga: update

* Grouple: bump

* Mintmanga: fix
2025-01-18 20:13:05 +00:00
pjy612 b24bc5b9ca
Jinman Tiantang: Fix image blank (#7067)
* fix image blank

* Kotlin Syntax
2025-01-18 20:13:05 +00:00
bapeey bcfcc110aa
IkigaiMangas: Update domain and fix preference (#7061)
* fix pref

* opa

* cache prefs

* lint
2025-01-18 20:13:05 +00:00
bapeey 7fc0445474
Olympus Scanlation: Update domain automatically (#7060)
* update domain

* here too

* cache pref
2025-01-18 20:13:05 +00:00
AwkwardPeak7 075bcde304
bump keyoapp 2025-01-18 20:13:05 +00:00
Dr1ks de1f46782f
Add Riot Hentai source (#7059)
* Add Riot Hentai

* Riot Hentai: add isNsfw
2025-01-18 20:13:05 +00:00
Dr1ks 57d4f6f855
Webtoon Hatti: fix popular and latest tabs (#7058) 2025-01-18 20:13:05 +00:00
dngonz 60dcde32b1
MyReadingManga: Not result for some filters (#7044)
fix filter with , in it
2025-01-18 20:13:05 +00:00
Cuong-Tran 4bc138aa39
Manga District: add tag browse (#7035)
* Manga District: add tag browse

* Apply suggestions from code review

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

* correction

* Separate method to load tags from preferences & always initialize with at least 1 tag to avoid excessively reading preferences

* remove unnecessary getter

* Fix: actually update backing field; also improve load tags from preferences

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:05 +00:00
Dr1ks 6f90a79c96
Desu: Add authors name (#7027)
* Desu: Add authors name

* Desu: update
2025-01-18 20:13:05 +00:00
Dr1ks 7f168d0cd1
Update MangaGezgini & Meitu domains (#7028) 2025-01-18 20:13:05 +00:00
dngonz 5a4c6a39bb
Jinman Tiantang: Fix image load (#7019)
fix image load
2025-01-18 20:13:05 +00:00
suhaien 8e86f6b723
Add 11toon (#7006)
* Add 11toon

* update search

* update parsers
2025-01-18 20:13:05 +00:00
Chopper d7f724243c
YushukeMangas: Fix content loading and bump versionID (#6986)
* Fix content loading and bump versionID

* Remove mutableList instance

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

* Remove use function

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

* Remove unused code

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:05 +00:00
Chopper 73013e9d46
SussyToons: Fix loading pages (#7041)
Fix loading pages
2025-01-18 20:13:05 +00:00
Chopper 509cc58346
SussyToons: Fix loading pages (#7015)
Fix loading pages
2025-01-18 20:13:05 +00:00
Chopper aa06c5631f
Add IgnisComic (#6999)
* Add IgnisComic

* Change from MangaThemesiaAlt to MangaThmesia
2025-01-18 20:13:05 +00:00
Dr1ks d328389cc7
Tortuga Ceviri: Fix chapters order (#7007) 2025-01-18 20:13:05 +00:00
dngonz 05aebf390a
PoseidonScans add (#7003)
* add poseidon scans

* rename
2025-01-18 20:13:05 +00:00
dngonz ad73175a1f
ReaperScans Unoriginal: Moved to ParsedHttpSource (#7002)
* move to ParsedHttpSource

* remove old code

* clean up
2025-01-18 20:13:05 +00:00
Chopper c88b8b28aa
VioletScans(ShojoScans): Update domain and rebranding (#7000)
* Update domain

* Rebranding

* Bump version

* Add id
2025-01-18 20:13:05 +00:00
iloverabbit f64996d23c
Popsmanga: Change base URL (#6996)
* Popsmanga: Change base URL

* Popsmanga: Bump version code to 1
2025-01-18 20:13:05 +00:00
dngonz f8bafa79ee
Keyoapp: Fix manga details (#6985)
* fix manga details

* change also description select
2025-01-18 20:13:05 +00:00
Vetle Ledaal ab7b4fa09e
Add Paradise BL (#6965) 2025-01-18 20:13:05 +00:00
dngonz efc3642c17
ManyToon: Change domain (#6983)
change domain
2025-01-18 20:13:05 +00:00
Romain 4a1a040b23
Patch ReaperScans (#6982)
* Patch ReaperScans

* Resolve some mistakes

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:05 +00:00
dngonz b7409c8efd
VCPVMP: Fix pages don't load (#6966)
* fix pages not all loading

* enhance pageListSelector VerComics
2025-01-18 20:13:05 +00:00
Chopper d9822c7ef5
SeitaCelestial: Add dateFormat (#6968)
Add dateFormat
2025-01-18 20:13:05 +00:00
morallkat ffd43e2873
zh/boylove: Fix parsing popular page (#6964)
zh/boylove: fix popular list
2025-01-18 20:13:05 +00:00
usagisang 5d402cc101
Komga: Add random sort option for Komga 1.16.0 (#6963)
Komga: Add random sort option
2025-01-18 20:13:04 +00:00
dngonz 3e2f37043b
Vortex Scans: Fix no pages found (#6948)
* fix images

* replace substring with regexp

* change approach

* use kotlinx.seriaze intead of replace for json

* update comment
2025-01-18 20:13:04 +00:00
Chopper f72e042cce
ArgosScan: Change CMS (#6931)
* Change CMS

* Use sortedByDescend

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:04 +00:00
suhaien 06dd598b45
Comicextra: Fixes and Update Filters (#6894)
* Comicextra: Fixes and Update Filters

Filter updates
- Add status filter (ongoing, completed)
- Search types can now be combined

* Update latest title selector

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

* remove latest update thumbnail

* update parsers

* update date parsing

* remove non-null assert in date

* remove LEOMACS filter

* update parsers

* update urlEl

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:04 +00:00
AwkwardPeak7 6b8b650004
add Wolfdotcom (#6534)
* wolfdotcom

* fix selectors

* domain preference and auto update

* update domain number

* auto update domain in ci

* update regex

* use

* current domain number

* don't set chapter number as it is more of an index than actual chapter num
2025-01-18 20:13:04 +00:00
Chopper bb2e8d2cde
SussyScan: Fix page loading (#6961)
Fix page loading
2025-01-18 20:13:04 +00:00
Chopper b4314f5f0b
Snowmtl: Use pt as fontsize (#6954)
* Use pt as fontsize

* Add comment
2025-01-18 20:13:04 +00:00
Dr1ks 0a3cc9a886
Serimanga: Fix latest and popular tabs (#6944) 2025-01-18 20:13:04 +00:00
Chopper b1f2459ddc
Add AniglisNovels and fix Keyoapp thumbnail quality (#6939)
Add AniglisNovels
2025-01-18 20:13:04 +00:00
duongtra df2a548325
TeamLanhLung: Update domain (#6938)
update domain
2025-01-18 20:13:04 +00:00
Vetle Ledaal 77ae53ba1e
Remove 4 dead extensions (#6937)
* Remove Mahua USS

* Remove Manga-1001.com

* Remove Manga Siginagi

* Remove MangaOwl.To
2025-01-18 20:13:04 +00:00
Vetle Ledaal 299d0f1ad8
Update domain Gri Melek/Taikutsu (#6936)
* Taikutsu: update domain

* Gri Melek: update domain
2025-01-18 20:13:04 +00:00
Fioren 0c56c453f5
Update domain (#6933)
Update domain TopTruyen and DocTruyen3Q
2025-01-18 20:13:04 +00:00
dngonz 280ae02b9b
Tojimangas: fix extension (#6926)
* fix extension

* requested changes
2025-01-18 20:13:04 +00:00
dngonz 2821b0dce8
Mitaku: Fix browse (#6924)
fix selector
2025-01-18 20:13:04 +00:00
Vetle Ledaal 5143199e08
Thunder-/Night-Scans: do not use :is() selector (#6920) 2025-01-18 20:13:04 +00:00
Vetle Ledaal 61c8073679
SlimeReadTheme: remove use of incompatible API (#6919)
Call requires API level 26 (current min is 21):
`java.util.regex.Matcher#start` (called from kotlin. text.
`MatchGroupCollection#get(String)`)
2025-01-18 20:13:04 +00:00
Chopper 0615bf338a
SlimeRead: Add SChapter::date_upload (#6918)
Add SChapter::date_upload
2025-01-18 20:13:04 +00:00
Chopper 4f8715d432
Add ManhwaToon (#6904) 2025-01-18 20:13:04 +00:00
Chopper 943b7992c8
Add RagnarScans (#6903) 2025-01-18 20:13:04 +00:00
dngonz ecfa117f5d
Fix domains (#6898)
* change domain manhwaindo

* change domain bacakomik

* change extName

Co-authored-by: Chopper  <156493704+choppeh@users.noreply.github.com>

---------

Co-authored-by: Chopper <156493704+choppeh@users.noreply.github.com>
2025-01-18 20:13:04 +00:00
kana-shii 32aa5f3808
Mangago: Regex update (#6878)
* regex update

* Update Mangago.kt

* Update build.gradle

* Update Mangago.kt
2025-01-18 20:13:04 +00:00
kana-shii 0002103804
Bato.to: Regex update (#6848)
* update regex

* Update build.gradle

* Update BatoTo.kt

* Update BatoTo.kt
2025-01-18 20:13:04 +00:00
Dr1ks 18b1977691
[RU]GroupLe fix chapter loads and manga status (#6828)
* [RU]GroupLe fix chapter loads and manga status

* [RU]GroupLe small fix of manga status

* [RU]GroupLe review fix

* [RU]GroupLe fix for allhentai

* [RU]GroupLe checks for allhentai and mintmanga
2025-01-18 20:13:04 +00:00
dngonz 64b447a4ac
Manhwabuddy: Fix res and add filters (#6910)
* fix res

* fix search and add genre filters

* bump
2025-01-18 20:13:04 +00:00
Chopper 32f9674e70
Add HianatoScan (#6901) 2025-01-18 20:13:04 +00:00
DokterKaj 6c67e88e5b
DeviantArt: Always use desktop user agent, only allow valid search queries (#6899)
* Always use desktop user agent, only allow valid search queries

* Re-add trailing comma
2025-01-18 20:13:04 +00:00
mrtear bd0f21c65e
Add Vortex Scans Free (#6889)
* Add Vortex Scans Free

* Change
2025-01-18 20:13:04 +00:00
mrtear a83f7d4f97
Roumanwu: Update Domain (#6885)
Update Roumanwu URL
2025-01-18 20:13:04 +00:00
Chopper ae7fd918dd
SussyScan: Migration (#6855)
* Migrate

* Use HttpUrl

* Sort chapters

* Change popularRequest

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

* Update src/pt/sussyscan/src/eu/kanade/tachiyomi/extension/pt/sussyscan/SussyScan.kt

Change searchMangaRequest

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

* change latestUpdatesRequest

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

* Add comment

* Change chapters url

* changes

* Fix pages header

* Use setUrlWithoutDomain

* Use HttpUrl in SChapter::id and remove variable shadowing

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:04 +00:00
Creepler13 a838bad72c
WeebCentral add info to mangadetails (#6840)
* Add requests

* chanmge

* a
2025-01-18 20:13:04 +00:00
Vetle Ledaal dcb623cdb3
Eros Scans: update domain, skip redirect on CDN (#6887) 2025-01-18 20:13:04 +00:00
Chopper 059e3267af
Add MonteTai (#6884) 2025-01-18 20:13:04 +00:00
mrtear 31fff66ff8
WebtoonHatti: Update URL (#6880)
Update WebtoonHatti URL
2025-01-18 20:13:04 +00:00
haruki-takeshi 5a3e302137
Update Comanhua URL (#6869)
* Update CoManhua.kt

* Update build.gradle
2025-01-18 20:13:04 +00:00
Michał Marszałek 696f4f2bc9
Fix InfinityScans Accept header (#6857) 2025-01-18 20:13:04 +00:00
Vetle Ledaal 44ad6961d3
Update domain for NineHentai (+revert removal) (#6842)
* Revert 9bc701d65ae9493772848b5d5fd927a87b3d7fb5 (partial)

* NineHentai: update domain
2025-01-18 20:13:04 +00:00
dngonz 39ae8f5e73
YakshaScans: Fix 403 Error (#6832)
add js challenge interceptor
2025-01-18 20:13:04 +00:00
dngonz 77debf098e
SamuraiScan: Fix 404 Error (#6831)
* fix manga substring

* change manga substring to rd
2025-01-18 20:13:04 +00:00
zhongfly c9a27a8b51
zaimanhua: add ranking filters (#6792) 2025-01-18 20:13:04 +00:00
Creepler13 4647af4f9c
mangapro to Iken multisrc (#6826)
mangapro to Iken
2025-01-18 20:13:04 +00:00
Chopper 68ccc79b5b
Snowmtl: Change default font family (#6822)
* Change default font family

* Fix filename
2025-01-18 20:13:04 +00:00
Vetle Ledaal 4e1beae2b7
Remove unused domains (#6820)
* Remove XKCD KO (domain for sale)

* Remove MangaDino.top (unoriginal) (domain for sale)

* Remove parked domains

Remove Atlantis Scan
Remove Boosei
Remove CreepyScans
Remove EarlyManga
Remove Glory Manga
Remove Heroxia
Remove Lucky Manga
Remove MangaDeemak
Remove MangaKitsune
Remove Manga Mitsu
Remove Mangá Nanquim
Remove ManhuaDex
Remove Mirai Scans
Remove Pony Manga
Remove RawZ
Remove TuManhwas

* Remove more parked domains (other TLDs)

Remove Komik Gue
Remove MangaOnline.team (unoriginal)
Remove Manga Rock.team (unoriginal)
Remove MangaYami
Remove Manhwua.fans
Remove Nekomik
2025-01-18 20:13:04 +00:00
FunnyTiming 70efa61570
Add YaoiScan (#6814)
* Add YaoiScan

* Wrong CMS for YaoiScan

* Patched status detection

* Update src/fr/yaoiscan/build.gradle

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

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:04 +00:00
DokterKaj 065ff132a0
DeviantArt: Fix NullPointerException for some galleries (#6812)
Select always-present element for gallery name
2025-01-18 20:13:04 +00:00
AwkwardPeak7 c3c87863b2
Hitomi: retry on stream reset error like the website (#6802) 2025-01-18 20:13:03 +00:00
AwkwardPeak7 919a6490bb
Happymh: use hashmap instead
LruCache is stub in the inspector
2025-01-18 20:13:03 +00:00
Chopper cf8b7f3f31
QuantumScans: Theme and domain changed (#6800)
Theme changed
2025-01-18 20:13:03 +00:00
Chopper b0250b98f7
Add SiteManga (#6799) 2025-01-18 20:13:03 +00:00
Chopper 5723bb392d
Add MangaTuk (#6798) 2025-01-18 20:13:03 +00:00
Chopper 7573bca926
Update domains (#6776)
* Update domains

* Migrate theme

* Fix Siikomik response code 500

* Holiday: Fix popularManga path segment
2025-01-18 20:13:03 +00:00
AlphaBoom f2208ff245
Happymh: Fix temporary chapter url issue. (#6731)
* Happymh: Fix temporary chapter url issue.

* Apply suggestions from code review

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

* Happymh: Apply review suggestions.

* Use Httpurl to parse.

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-01-18 20:13:03 +00:00
renovate[bot] a90166842c
Update dependency gradle to v8.12 (#6745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-18 20:13:03 +00:00
Eshlender d9661d2617
[RU]LibGroup add auto image servers (#6747)
* [RU]LibGroup add auto image servers

* summary auto

* isServers != "auto"

* setDefaultValue("compress")

* change Timeouts

* url(url).head()

* val url(url).head()
2025-01-18 20:13:03 +00:00
zhongfly 7813eb9117
zaimanhua: fix empty author and upload time (#6786) 2025-01-18 20:13:03 +00:00
Chopper 820806815b
Add ShindaiScan (#6783) 2025-01-18 20:13:03 +00:00
Chopper a3a2580c84
Slimeread: Move slimeread to lib-multsrc and add mahouscan (#6780)
Move slimeread to lib-multsrc and add mahouscan
2025-01-18 20:13:03 +00:00
Creepler13 01a1ae5d1c
Fix Iken Popular Page (#6779)
* Fix Iken Popular, remove from Hive

* update philliascans url
2025-01-18 20:13:03 +00:00
Chopper c97115f9ba
Add MangaMania (#6778)
* Add MangaMania

* Fix lint
2025-01-18 20:13:03 +00:00
DokterKaj 9ead615784
Add DeviantArt (#6694)
* Add DeviantArt

* Slight cleanup

* Use .absUrl(), remove not-null asserts

* Use less volatile selectors

* Use better selector for gallery name

* Remove not-null assert on subFolderGallery

* Remove autoVerify from manifest

* Remove unnecessary RSS request, simplify query parsing

* Fetch HQ image

* Account for gallery:{username} and gallery:{username}/all

* Reword search fallback message

* Allow parseDate() to accept null
2025-01-18 20:13:03 +00:00
Lev 0c399f549e
Update ReadChainsawManMangaOnline extension (#6665)
* Update ReadChainsawManMangaOnline source

* Updated domain

* Update domain name in the class constructor and changed titles to match source naming

* Update ReadChainsawManMangaOnline nsfw flag

* Update src/en/readchainsawmanmangaonline/build.gradle

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-01-18 20:13:03 +00:00
lord-ne e78ddcb9ed
[MangaThemesia] Option to hide paid chapters (#6598)
* Initial implementation

* Move implementation to helper

* Update gradle files

* Formatting

* Use base class
2025-01-18 20:13:03 +00:00
duongtra 9ad46416f5
TeamLanhLung: Update domain (#6752)
update domain
2025-01-18 20:13:03 +00:00
Chaos Pjeles 512716f5aa
fixes image load (403) for MadaraDex (#6749)
fixes image-load (403), closes #6670
2025-01-18 20:13:03 +00:00
AwkwardPeak7 af92d1591d
Bato: update domain mirrors (#6748)
* batoto: update domain mirrors

* cleanup prefs

* tfw
2025-01-18 20:13:03 +00:00
alberlandohc caf440aaa2
Readmangas: Fixed to the pagination of the chapters and the domain (#6725)
Fix return type in pageListParse to List<Page> to match method signature
2025-01-18 20:13:03 +00:00
Creepler13 34abdec28c
Add MangaKuro (#6717)
* Add MangaKuro

* a

* non null
2025-01-18 20:13:03 +00:00
bapeey d89120eb49
EternalMangas: Fix search and filter tab (#6743)
fix search
2025-01-18 20:13:01 +00:00
bapeey a383626ae6
Add UchuujinProjects (#6742)
add UchuujinProjects
2025-01-18 20:13:01 +00:00
bapeey 6a476ee786
IkigaiMangas: Update default domain (#6741)
update default domain
2025-01-18 20:13:01 +00:00
Vetle Ledaal 9688652e27
Remove Animated Glitched Scans (#6740) 2025-01-18 20:13:01 +00:00
Vetle Ledaal 10542c7aa4
Remove Asure Scans.gg (unoriginal) (#6738) 2025-01-18 20:13:01 +00:00
alberlandohc c8654953b2
Mangakoma: Update url manga koma (#6720)
update url manga koma
2025-01-18 20:13:01 +00:00
alberlandohc 8a30734f59
Shinigami: Update url shinigami (#6718)
update url shinigami
2025-01-18 20:13:01 +00:00
alberlandohc cd90bcf6ca
Knightnoscanlation: Update url Knight No Scanlation (#6716)
update url Knight No Scanlation
2025-01-18 20:13:01 +00:00
alberlandohc 4cc3316b5e
Domalfansub: Update url Domal Fansub (#6714)
update url Domal Fansub
2025-01-18 20:13:01 +00:00
alberlandohc 2b88f32401
Lxhentai: Update url lxhentai (#6713)
update url lxhentai
2025-01-18 20:13:01 +00:00
alberlandohc 30e7a06b74
Truyenvn: Update url truyenvn (#6712)
update url truyenvn
2025-01-18 20:13:01 +00:00
Shahzaib 2a44520bbe
Add Asura Scans Free (#6706) 2025-01-18 20:13:01 +00:00
SilverBeamx 003545ac44
GalleryAdults & Fox additions (#6686)
* Expand tag search to 5 pages as 3 are not enough for HentaiFox

* Make launchIO protected instead of private

* Add Top Rated, Most Faved, Most Fapped, Most Downloaded search

* Update versioning

* Address comments

* Move sidebar categories to a map

* Fetch csrf token from tags page

* Move launchIO back to private as it is no longer needed

* Clean position of csrf functions

* Use map with key lookup for popular categories
2025-01-18 20:13:01 +00:00
Further 5588f4d762
Baozi: add new domain (#6658)
* Baozi: add new domain

* Baozi: add changelog

* Baozi: remove old domain

* Baozi: Update CHANGELOG.md
2025-01-18 20:13:01 +00:00
are-are-are bfe8f5d00f
Hitomi: Fix missing field error (#6681)
Quick fix
2025-01-18 20:13:01 +00:00
Cuong-Tran 7ec1dfaf48
Explicit cloudflareClient if Cloudflare hosted (#6676) 2025-01-18 20:13:01 +00:00
FourTOne5 be9c14bcae
WebNovel: Fix cover url and refactor (#6672) 2025-01-18 20:13:01 +00:00
Vetle Ledaal f03fd3c5f7
Asura Scans: support high quality chapter images (#6657)
* Asura Scans: support high quality chapter images

* Only rewrite chapter images, add fallback if broken - explained in ext settings
2025-01-18 20:13:01 +00:00
kana-shii 0382073769
add Rose Squad Scans (#6655)
add rose squad scans
2025-01-18 20:13:01 +00:00
Creepler13 4f5116c590
InfernalVoidScans: Switch to iken MultiSrc (#6642)
* switch to iken

* public titleCache

* protected

* Change to Hive Scans
2025-01-18 20:13:01 +00:00
FourTOne5 931711fe74
Update CONTRIBUTING.md 2025-01-18 20:13:01 +00:00
Cezary 2430b18af0
MyReadingManga fix webview block (#6617)
* MyReadingManga fix bug "Not found"

* MyReadingManga fix bug "Not found" in view-mode
2025-01-18 20:13:01 +00:00
Vetle Ledaal 8b212ffdcd
Explicit cloudflareClient if Cloudflare hosted (#6613)
* Explicit cloudflareClient if Cloudflare hosted

* avoid modifying multisrc sources
2025-01-18 20:13:01 +00:00
Chopper c432620356
Snowmtl: Adds support for multiple languages (#6563)
* Move to src/all

* Optimize translation

* Fix image loading timeout and expired translator token

* Fix extension initialization

* Fix translator response
2025-01-18 20:13:01 +00:00
Creepler13 f228ad572d
Asurascans Hide Premium chapters (#6627)
* Hide Premium chapters

* default true, Formatting
2025-01-18 20:13:01 +00:00
Cuong-Tran d65b847907
ManyComic: fix Madara theme (#6624) 2025-01-18 20:13:00 +00:00
Eshlender f94b827056
[RU]NewManga(Newbie) closed (#6620) 2025-01-18 20:13:00 +00:00
Eshlender dfc8f73cb5
[RU]MangaLib fix cloudflare errors (#6608)
* [RU]MangaLib fix cloudflare errors

* test-front equal main domain

* add api domains

* api domains summary

* fix

* change PREF

* add api "https://api.mangalib.me"
2025-01-18 20:13:00 +00:00
Vetle Ledaal d3054332eb
Asura Scans: fix chapter selector (#6607) 2025-01-18 20:13:00 +00:00
841 changed files with 8148 additions and 5786 deletions

View File

@ -68,7 +68,7 @@ small, just do a normal full clone instead.**
1. Do a partial clone.
```bash
git clone --filter=blob:none --sparse <fork-repo-url>
cd extensions/
cd extensions-source/
```
2. Configure sparse checkout.

View File

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

View File

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

3
gradlew generated vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View File

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

View File

@ -37,6 +37,8 @@ abstract class BlogTruyen(
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")

View File

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

View File

@ -814,8 +814,8 @@ abstract class GalleryAdults(
val tags = mutableListOf<Genre>()
runBlocking {
val jobsPool = mutableListOf<Job>()
// Get first 3 pages
(1..3).forEach { page ->
// Get first 5 pages
(1..5).forEach { page ->
jobsPool.add(
launchIO {
runCatching {

View File

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

View File

@ -177,17 +177,18 @@ abstract class GroupLe(
"div#tab-description .manga-description",
).text()
manga.status = when {
infoElement.html()
.contains("Запрещена публикация произведения по копирайту") || infoElement.html()
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!") -> SManga.LICENSED
infoElement.html().contains("<b>Сингл</b>") -> SManga.COMPLETED
(
document.html()
.contains("Запрещена публикация произведения по копирайту") || document.html()
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!")
) && document.select("div.chapters").isEmpty() -> SManga.LICENSED
infoElement.html().contains("<b>Сингл") -> SManga.COMPLETED
else ->
when (infoElement.select("p:contains(Перевод:) span").first()?.text()) {
"продолжается" -> SManga.ONGOING
"начат" -> SManga.ONGOING
"переведено" -> SManga.COMPLETED
"завершён" -> SManga.COMPLETED
"приостановлен" -> SManga.ON_HIATUS
when (infoElement.selectFirst("span.badge:contains(выпуск)")?.text()) {
"выпуск продолжается" -> SManga.ONGOING
"выпуск начат" -> SManga.ONGOING
"выпуск завершён" -> if (infoElement.selectFirst("span.badge:contains(переведено)")?.text()?.isNotEmpty() == true) SManga.COMPLETED else SManga.PUBLISHING_FINISHED
"выпуск приостановлен" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
@ -213,15 +214,9 @@ abstract class GroupLe(
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val document = response.asJsoup()
if ((
document.select(".expandable.hide-dn").isNotEmpty() && document.select(".user-avatar")
.isEmpty() && document.toString()
.contains("current_user_country_code = 'RU'")
) || (
document.select("img.logo")
.first()?.attr("title")
?.contains("Allhentai") == true && document.select(".user-avatar").isEmpty()
)
if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
}
@ -313,20 +308,22 @@ abstract class GroupLe(
val html = document.html()
val readerMark = "rm_h.readerDoInit(["
if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
if (!html.contains(readerMark)) {
if (document.select(".input-lg").isNotEmpty() || (
document.select(".user-avatar")
.isEmpty() && document.select("img.logo").first()?.attr("title")
?.contains("Allhentai") == true
)
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
}
if (!response.request.url.toString().contains(baseUrl)) {
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
}
val readerMark = when {
html.contains("rm_h.readerDoInit([") -> "rm_h.readerDoInit(["
html.contains("rm_h.readerInit([") -> "rm_h.readerInit(["
!response.request.url.toString().contains(baseUrl) -> {
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
}
else -> {
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
}
}
val beginIndex = html.indexOf(readerMark)

View File

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

View File

@ -33,7 +33,7 @@ abstract class Iken(
.set("Referer", "$baseUrl/")
private var genres = emptyList<Pair<String, String>>()
private val titleCache by lazy {
protected val titleCache by lazy {
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
val data = response.parseAs<SearchResponse>()
@ -53,11 +53,9 @@ abstract class Iken(
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
.map { it.absUrl("href").substringAfterLast("/series/") }
val entries = slugs.mapNotNull {
titleCache[it]?.toSManga()
val entries = document.select("aside a:has(img)").mapNotNull {
titleCache[it.absUrl("href").substringAfter("series/")]?.toSManga()
}
return MangasPage(entries, false)

View File

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

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -205,16 +206,22 @@ abstract class Keyoapp(
}
// Details
protected open val descriptionSelector: String = "div:containsOwn(Synopsis) ~ div"
protected open val statusSelector: String = "div:has(span:containsOwn(Status)) ~ div"
protected open val authorSelector: String = "div:has(span:containsOwn(Author)) ~ div"
protected open val artistSelector: String = "div:has(span:containsOwn(Artist)) ~ div"
protected open val genreSelector: String = "div:has(span:containsOwn(Type)) ~ div"
protected open val dateSelector: String = ".text-xs"
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.selectFirst("div.grid > h1")!!.text()
thumbnail_url = document.getImageUrl("div[class*=photoURL]")
description = document.selectFirst("div.grid > div.overflow-hidden > p")?.text()
status = document.selectFirst("div[alt=Status]").parseStatus()
author = document.selectFirst("div[alt=Author]")?.text()
artist = document.selectFirst("div[alt=Artist]")?.text()
description = document.selectFirst(descriptionSelector)?.text()
status = document.selectFirst(statusSelector).parseStatus()
author = document.selectFirst(authorSelector)?.text()
artist = document.selectFirst(artistSelector)?.text()
genre = buildList {
document.selectFirst("div[alt='Series Type']")?.text()?.replaceFirstChar {
document.selectFirst(genreSelector)?.text()?.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
@ -227,7 +234,7 @@ abstract class Keyoapp(
}.joinToString()
}
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
protected fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"ongoing" -> SManga.ONGOING
"dropped" -> SManga.CANCELLED
"paused" -> SManga.ON_HIATUS
@ -247,7 +254,7 @@ abstract class Keyoapp(
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
name = element.selectFirst(".text-sm")!!.text()
element.selectFirst(".text-xs")?.run {
element.selectFirst(dateSelector)?.run {
date_upload = text().trim().parseDate()
}
if (element.select("img[src*=Coin.svg]").isNotEmpty()) {
@ -308,6 +315,12 @@ 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
?.toHttpUrlOrNull()?.let {
it.newBuilder()
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
.build()
.toString()
}
}
}
@ -325,8 +338,6 @@ abstract class Keyoapp(
private fun String.parseRelativeDate(): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}

View File

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

View File

@ -34,7 +34,6 @@ import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
@ -65,43 +64,46 @@ abstract class LibGroup(
override val supportsLatest = true
private val userAgentMobile = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.3"
private var bearerToken: String? = null
private var userId: Int? = null
abstract val siteId: Int // Important in api calls
private val apiDomain: String = "https://api.lib.social"
private val apiDomain: String = preferences.getString(API_DOMAIN_PREF, API_DOMAIN_DEFAULT).toString()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(3)
.rateLimitHost(apiDomain.toHttpUrl(), 1)
.connectTimeout(5, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(::checkForToken)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.code == 419) {
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimit(3)
.rateLimitHost(apiDomain.toHttpUrl(), 1)
.rateLimitHost(baseUrl.toHttpUrl(), 1)
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(::checkForToken)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.code == 419) {
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
}
if (response.code == 404) {
throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
}
return@addInterceptor response
}
if (response.code == 404) {
throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
}
return@addInterceptor response
}
.build()
.build()
}
override fun headersBuilder() = Headers.Builder().apply {
// User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
add("User-Agent", userAgentMobile)
add("Accept", "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
add("Referer", baseUrl)
add("Site-Id", siteId.toString())
}
private fun imageHeader() = Headers.Builder().apply {
add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
add("Referer", baseUrl)
}.build()
private var _constants: Constants? = null
private fun getConstants(): Constants? {
if (_constants == null) {
@ -373,10 +375,25 @@ abstract class LibGroup(
return chapter
}
private fun checkImage(url: String): Boolean {
val getUrlHead = Request.Builder().url(url).head().headers(imageHeader()).build()
val response = client.newCall(getUrlHead).execute()
return response.isSuccessful && (response.header("content-length", "0")?.toInt()!! > 600)
}
override fun fetchImageUrl(page: Page): Observable<String> {
if (page.imageUrl != null) {
return Observable.just(page.imageUrl)
}
if (isServer() == "auto") {
for (serverApi in IMG_SERVERS.slice(1 until IMG_SERVERS.size)) {
val server = getConstants()?.getServer(serverApi, siteId)?.url
val imageUrl = "$server${page.url}"
if (checkImage(imageUrl)) {
return Observable.just(imageUrl)
}
}
}
val server = getConstants()?.getServer(isServer(), siteId)?.url ?: throw Exception("Ошибка получения сервера изображений")
return Observable.just("$server${page.url}")
}
@ -384,13 +401,7 @@ abstract class LibGroup(
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val imageHeader = Headers.Builder().apply {
// User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
add("User-Agent", userAgentMobile)
add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
add("Referer", baseUrl)
}
return GET(page.imageUrl!!, imageHeader.build())
return GET(page.imageUrl!!, imageHeader())
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
@ -565,6 +576,7 @@ abstract class LibGroup(
companion object {
const val PREFIX_SLUG_SEARCH = "slug:"
private const val SERVER_PREF = "MangaLibImageServer"
private val IMG_SERVERS = arrayOf("auto", "main", "secondary", "compress")
private const val SORTING_PREF = "MangaLibSorting"
private const val SORTING_PREF_TITLE = "Способ выбора переводчиков"
@ -578,12 +590,16 @@ abstract class LibGroup(
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
private const val LANGUAGE_PREF_TITLE = "Выбор языка на обложке"
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
private const val API_DOMAIN_TITLE = "Выбор домена API"
private const val API_DOMAIN_DEFAULT = "https://api.imglib.info"
private const val TOKEN_STORE = "TokenStore"
val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US) }
}
private fun isServer(): String = preferences.getString(SERVER_PREF, "main")!!
private fun isServer(): String = preferences.getString(SERVER_PREF, "compress")!!
private fun isEng(): String = preferences.getString(LANGUAGE_PREF, "eng")!!
private fun groupTranslates(): String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
private fun isScanUser(): Boolean = preferences.getBoolean(IS_SCAN_USER, false)
@ -591,12 +607,18 @@ abstract class LibGroup(
val serverPref = ListPreference(screen.context).apply {
key = SERVER_PREF
title = "Сервер изображений"
entries = arrayOf("Первый", "Второй", "Сжатия")
entryValues = arrayOf("main", "secondary", "compress")
summary = "%s \n\nВыбор приоритетного сервера изображений. \n" +
"По умолчанию «Первый». \n\n" +
entries = arrayOf("Автовыбор", "Первый", "Второй", "Сжатия")
entryValues = IMG_SERVERS
summary = "%s \n\n" +
"По умолчанию в приложении и на сайте «Сжатия» - самый стабильный и быстрый. \n\n" +
"«Автовыбор» - проходит по всем серверам и показывает только загруженную картинку. \nМожет происходить медленно. \n\n" +
"ⓘВыбор другого сервера помогает при ошибках и медленной загрузки изображений глав."
setDefaultValue("main")
setDefaultValue("compress")
setOnPreferenceChangeListener { _, newValue ->
val warning = "Для смены сервера: Настройки -> Дополнительно -> Очистить кэш глав"
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
true
}
}
val sortingPref = ListPreference(screen.context).apply {
@ -629,11 +651,30 @@ abstract class LibGroup(
true
}
}
val domainApiPref = ListPreference(screen.context).apply {
key = API_DOMAIN_PREF
title = API_DOMAIN_TITLE
entries = arrayOf("Официальное приложение (api.imglib.info)", "Основной (api.lib.social)", "Резервный (api.mangalib.me)", "Резервный 2 (api2.mangalib.me)")
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api.lib.social", "https://api.mangalib.me", "https://api2.mangalib.me")
summary = "%s" +
"\n\nВыбор домена API, используемого для работы приложения." +
"\n\nПо умолчанию «Официальное приложение»" +
"\n\nⓘВы не увидите его нигде глазами, но источник должен начать работать стибильнее."
setDefaultValue(API_DOMAIN_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(serverPref)
screen.addPreference(sortingPref)
screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates()))
screen.addPreference(scanlatorUsername)
screen.addPreference(titleLanguagePref)
screen.addPreference(domainApiPref)
}
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String): androidx.preference.EditTextPreference {
return androidx.preference.EditTextPreference(context).apply {

View File

@ -45,7 +45,7 @@ class Constants(
)
fun getServer(isServers: String?, siteId: Int): ImageServer =
if (!isServers.isNullOrBlank()) {
if (!isServers.isNullOrBlank() and (isServers != "auto")) {
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
} else {
imageServers.first { it.siteIds.contains(siteId) }

View File

@ -3,7 +3,7 @@
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.mangasee.MangaseeUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslationsUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -12,11 +12,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="mangasee123.com"
android:pathPattern="/manga/..*"
android:scheme="https" />
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.extension.en.snowmtl
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.os.Build
import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -24,22 +24,21 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@RequiresApi(Build.VERSION_CODES.M)
class Snowmtl : ParsedHttpSource() {
override val name = "Snow Machine Translations"
override val baseUrl = "https://snowmtl.ru"
override val lang = "en"
@RequiresApi(Build.VERSION_CODES.O)
abstract class MachineTranslations(
override val name: String,
override val baseUrl: String,
val language: Language,
) : ParsedHttpSource() {
override val supportsLatest = true
private val json: Json by injectLazy()
override val lang = language.lang
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.addInterceptor(ComposedImageInterceptor(baseUrl, super.client))
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
.build()
// ============================== Popular ===============================
@ -158,7 +157,9 @@ class Snowmtl : ParsedHttpSource() {
dto.imageUrl.startsWith("http") -> dto.imageUrl
else -> "https://${dto.imageUrl}"
}
val fragment = json.encodeToString<List<Translation>>(dto.translations)
val fragment = json.encodeToString<List<Dialog>>(
dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
)
Page(index, imageUrl = "$imageUrl#$fragment")
}
}
@ -203,6 +204,7 @@ class Snowmtl : ParsedHttpSource() {
}
companion object {
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
const val PREFIX_SEARCH = "id:"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.snowmtl
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.graphics.Color
import android.os.Build
@ -8,35 +8,43 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import java.io.IOException
@Serializable
class PageDto(
@SerialName("img_url")
val imageUrl: String,
@Serializable(with = TranslationsListSerializer::class)
val translations: List<Translation> = emptyList(),
@SerialName("translations")
@Serializable(with = DialogListSerializer::class)
val dialogues: List<Dialog> = emptyList(),
)
@Serializable
@RequiresApi(Build.VERSION_CODES.O)
class Translation(
data class Dialog(
val x1: Float,
val y1: Float,
val x2: Float,
val y2: Float,
val text: String,
val angle: Float = 0f,
val isBold: Boolean = false,
val isNewApi: Boolean = false,
val type: String = "sub",
val textByLanguage: Map<String, String> = emptyMap(),
val type: String = "normal",
private val fbColor: List<Int> = emptyList(),
private val bgColor: List<Int> = emptyList(),
) {
val text: String get() = textByLanguage["text"] ?: throw Exception("Dialog not found")
fun getTextBy(language: Language) = textByLanguage[language.target] ?: text
val width get() = x2 - x1
val height get() = y2 - y1
val centerY get() = (y2 + y1) / 2f
@ -55,41 +63,59 @@ class Translation(
}
}
private object TranslationsListSerializer :
JsonTransformingSerializer<List<Translation>>(ListSerializer(Translation.serializer())) {
private object DialogListSerializer :
JsonTransformingSerializer<List<Dialog>>(ListSerializer(Dialog.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray(
element.jsonArray.map { jsonElement ->
val (coordinates, text) = getCoordinatesAndCaption(jsonElement)
val coordinates = getCoordinates(jsonElement)
val textByLanguage = getDialogs(jsonElement)
buildJsonObject {
put("x1", coordinates[0])
put("y1", coordinates[1])
put("x2", coordinates[2])
put("y2", coordinates[3])
put("text", text)
put("textByLanguage", textByLanguage)
try {
val obj = jsonElement.jsonObject
if (jsonElement.isArray) {
return@buildJsonObject
}
jsonElement.jsonObject.let { obj ->
obj["fg_color"]?.let { put("fbColor", it) }
obj["bg_color"]?.let { put("bgColor", it) }
obj["angle"]?.let { put("angle", it) }
obj["type"]?.let { put("type", it) }
obj["is_bold"]?.let { put("isBold", it) }
put("isNewApi", true)
} catch (_: Exception) { }
}
}
},
)
}
private fun getCoordinatesAndCaption(element: JsonElement): Pair<JsonArray, JsonElement> {
return try {
val arr = element.jsonArray
arr[0].jsonArray to arr[1]
} catch (_: Exception) {
val obj = element.jsonObject
obj["bbox"]!!.jsonArray to obj["text"]!!
private fun getCoordinates(element: JsonElement): JsonArray {
return when (element) {
is JsonArray -> element.jsonArray[0].jsonArray
else -> element.jsonObject["bbox"]?.jsonArray
?: throw IOException("Dialog box position not found")
}
}
private fun getDialogs(element: JsonElement): JsonObject {
return buildJsonObject {
when (element) {
is JsonArray -> put("text", element.jsonArray[1])
else -> {
element.jsonObject.entries
.filter { it.value.isString }
.forEach { put(it.key, it.value) }
}
}
}
}
private val JsonElement.isArray get() = this is JsonArray
private val JsonElement.isObject get() = this is JsonObject
private val JsonElement.isString get() = this.isObject.not() && this.isArray.not() && this.jsonPrimitive.isString
}

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
class MachineTranslationsFactoryUtils
data class Language(val lang: String, val target: String = lang, val origin: String = "en")

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.snowmtl
package eu.kanade.tachiyomi.multisrc.machinetranslations
import eu.kanade.tachiyomi.source.model.Filter

View File

@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.extension.es.tumanhwas
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import kotlin.system.exitProcess
class TuManhwasUrlActivity : Activity() {
@RequiresApi(Build.VERSION_CODES.O)
class MachineTranslationsUrlActivity : Activity() {
private val tag = javaClass.simpleName
@ -18,7 +20,7 @@ class TuManhwasUrlActivity : Activity() {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${TuManhwas.URL_SEARCH_PREFIX}$item")
putExtra("query", "${MachineTranslations.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.snowmtl
package eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@ -12,12 +12,14 @@ import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy
@ -29,11 +31,11 @@ import java.io.InputStream
import kotlin.math.pow
import kotlin.math.sqrt
// The Interceptor joins the captions and pages of the manga.
// The Interceptor joins the dialogues and pages of the manga.
@RequiresApi(Build.VERSION_CODES.O)
class ComposedImageInterceptor(
baseUrl: String,
private val client: OkHttpClient,
val language: Language,
) : Interceptor {
private val json: Json by injectLazy()
@ -44,48 +46,42 @@ class ComposedImageInterceptor(
"normal" to Pair<String, Typeface?>("$baseUrl/images/normal.ttf", null),
)
private val imageRegex = Regex(
"$baseUrl.*?\\.(webp|png|jpg|jpeg)#\\[.*?]",
RegexOption.IGNORE_CASE,
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toString()
val isPageImageUrl = imageRegex.containsMatchIn(url)
if (isPageImageUrl.not()) {
if (PAGE_REGEX.containsMatchIn(url).not()) {
return chain.proceed(request)
}
val translation = request.url.fragment?.parseAs<List<Translation>>()
?: throw IOException("Translation not found")
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
?: throw IOException("Dialogues not found")
val imageRequest = request.newBuilder()
.url(url)
.build()
// Load the fonts before opening the connection to load the image,
// so there aren't two open connections inside the interceptor.
loadAllFont(chain)
val response = chain.proceed(imageRequest)
if (response.isSuccessful.not()) {
return response
}
loadAllFont(chain)
val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!!
.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(bitmap)
translation
.filter { it.text.isNotBlank() }
.forEach { caption ->
val textPaint = createTextPaint(selectFontFamily(caption.type))
val dialogBox = createDialogBox(caption, textPaint, bitmap)
val y = getYAxis(textPaint, caption, dialogBox)
canvas.draw(dialogBox, caption, caption.x1, y)
}
dialogues.forEach { dialog ->
val textPaint = createTextPaint(selectFontFamily(dialog.type))
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
val y = getYAxis(textPaint, dialog, dialogBox)
canvas.draw(dialogBox, dialog, dialog.x1, y)
}
val output = ByteArrayOutputStream()
@ -108,7 +104,7 @@ class ComposedImageInterceptor(
}
private fun createTextPaint(font: Typeface?): TextPaint {
val defaultTextSize = 50.sp // arbitrary
val defaultTextSize = 24.pt // arbitrary
return TextPaint().apply {
color = Color.BLACK
textSize = defaultTextSize
@ -131,12 +127,13 @@ class ComposedImageInterceptor(
}
private fun loadAllFont(chain: Interceptor.Chain) {
val fallback = loadFont("coming_soon_regular.ttf")
fontFamily.keys.forEach { key ->
val font = fontFamily[key] ?: return@forEach
if (font.second != null) {
return@forEach
}
fontFamily[key] = key to (loadRemoteFont(font.first, chain) ?: loadFont("coming_soon_regular.ttf"))
fontFamily[key] = key to (loadRemoteFont(font.first, chain) ?: fallback)
}
}
@ -170,11 +167,17 @@ class ComposedImageInterceptor(
private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? {
return try {
val request = GET(fontUrl, chain.request().headers)
val response = client
.newCall(request).execute()
.takeIf(Response::isSuccessful) ?: return null
val response = chain.proceed(request)
if (response.isSuccessful.not()) {
response.close()
return null
}
val fontName = request.url.pathSegments.last()
response.body.byteStream().toTypeface(fontName)
response.body.use {
it.byteStream().toTypeface(fontName)
}
} catch (e: Exception) {
null
}
@ -189,63 +192,66 @@ class ComposedImageInterceptor(
/**
* Adjust the text to the center of the dialog box when feasible.
*/
private fun getYAxis(textPaint: TextPaint, caption: Translation, dialogBox: StaticLayout): Float {
private fun getYAxis(textPaint: TextPaint, dialog: Dialog, dialogBox: StaticLayout): Float {
val fontHeight = textPaint.fontMetrics.let { it.bottom - it.top }
val dialogBoxLineCount = caption.height / fontHeight
val dialogBoxLineCount = dialog.height / fontHeight
/**
* Centers text in y for captions smaller than the dialog box
* Centers text in y for dialogues smaller than the dialog box
*/
return when {
dialogBox.lineCount < dialogBoxLineCount -> caption.centerY - dialogBox.lineCount / 2f * fontHeight
else -> caption.y1
dialogBox.lineCount < dialogBoxLineCount -> dialog.centerY - dialogBox.lineCount / 2f * fontHeight
else -> dialog.y1
}
}
private fun createDialogBox(caption: Translation, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
var dialogBox = createBoxLayout(caption, textPaint)
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
var dialogBox = createBoxLayout(dialog, textPaint)
/**
* The best way I've found to adjust the text in the dialog box (Especially in long dialogues)
*/
while (dialogBox.height > caption.height) {
while (dialogBox.height > dialog.height) {
textPaint.textSize -= 0.5f
dialogBox = createBoxLayout(caption, textPaint)
dialogBox = createBoxLayout(dialog, textPaint)
}
// Use source setup
if (caption.isNewApi) {
textPaint.color = caption.foregroundColor
textPaint.bgColor = caption.backgroundColor
textPaint.style = if (caption.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
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(caption, bitmap)
textPaint.adjustTextColor(dialog, bitmap)
return dialogBox
}
private fun createBoxLayout(caption: Translation, textPaint: TextPaint) =
StaticLayout.Builder.obtain(caption.text, 0, caption.text.length, textPaint, caption.width.toInt()).apply {
private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint): StaticLayout {
val text = dialog.getTextBy(language)
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, dialog.width.toInt()).apply {
setAlignment(Layout.Alignment.ALIGN_CENTER)
setIncludePad(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
}
}.build()
}
// Invert color in black dialog box.
private fun TextPaint.adjustTextColor(caption: Translation, bitmap: Bitmap) {
val pixelColor = bitmap.getPixel(caption.centerX.toInt(), caption.centerY.toInt())
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, caption.foregroundColor) > minDistance) {
if (colorDistance(pixelColor, dialog.foregroundColor) > minDistance) {
return
}
color = inverseColor
@ -255,15 +261,16 @@ class ComposedImageInterceptor(
return json.decodeFromString(this)
}
private fun Canvas.draw(layout: StaticLayout, caption: Translation, x: Float, y: Float) {
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
save()
translate(x, y)
rotate(caption.angle)
rotate(dialog.angle)
layout.draw(this)
restore()
}
private val Int.sp: Float get() = this * SCALED_DENSITY
// https://pixelsconverter.com/pt-to-px
private val Int.pt: Float get() = this / SCALED_DENSITY
// ============================= Utils ======================================
@ -288,7 +295,8 @@ class ComposedImageInterceptor(
}
companion object {
const val SCALED_DENSITY = 1.5f // arbitrary
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt
val mediaType = "image/png".toMediaType()
}
}

View File

@ -46,6 +46,8 @@ abstract class MangaEsp(
protected open val seriesPath = "/ver"
protected open val useApiSearch = false
override val client: OkHttpClient = network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
@ -62,7 +64,9 @@ abstract class MangaEsp(
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga(seriesPath) }
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
.additionalParse()
.map { it.toSManga(seriesPath) }
return MangasPage(mangas, false)
}
@ -72,7 +76,9 @@ abstract class MangaEsp(
override fun latestUpdatesParse(response: Response): MangasPage {
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
val mangas = responseData.response.map { it.toSManga(seriesPath) }
val mangas = responseData.response
.additionalParse()
.map { it.toSManga(seriesPath) }
return MangasPage(mangas, false)
}
@ -93,20 +99,33 @@ abstract class MangaEsp(
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (useApiSearch) {
GET("$apiBaseUrl$apiPath/comics", headers)
} else {
GET("$baseUrl/comics", headers)
}
}
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
protected open fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
val document = response.asJsoup()
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
comicsList = if (useApiSearch) {
json.decodeFromString<List<SeriesDto>>(response.body.string()).toMutableList()
} else {
val script = response.asJsoup().select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
}.additionalParse().toMutableList()
return parseComicsList(page, query, filters)
}
protected open fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
return this
}
private var filteredList = mutableListOf<SeriesDto>()
protected open fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {

View File

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

View File

@ -50,7 +50,7 @@ abstract class MangaHub(
private var baseApiUrl = "https://api.mghcdn.com"
private var baseCdnUrl = "https://imgx.mghcdn.com"
override val client: OkHttpClient = super.client.newBuilder()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
userAgentType = UserAgentType.DESKTOP,
filterInclude = listOf("chrome"),

View File

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

View File

@ -22,6 +22,8 @@ abstract class MangaReader : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.cloudflareClient
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
final override fun popularMangaParse(response: Response) = searchMangaParse(response)

View File

@ -30,3 +30,5 @@ project_filter_warning=NOTE: Can't be used with other filter!
project_filter_name=%s Project List page
pref_dynamic_url_title=Automatically update dynamic URLs
pref_dynamic_url_summary=Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and "in library" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source.
pref_hide_paid_chapters_title=Hide chapters which require a purchase
pref_hide_paid_chapters_summary=Hide chapters which must be purchased using coins.\nYou might want to disable this if you want to be notified of paid chapters so that you can go purchase them.

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
class MangaThemesiaPaidChapterHelper(
private val hidePaidChaptersPrefKey: String = "pref_hide_paid_chapters",
private val lockedChapterSelector: String = "a[data-bs-target='#lockedChapterModal']",
) {
fun addHidePaidChaptersPreferenceToScreen(screen: PreferenceScreen, intl: Intl) {
SwitchPreferenceCompat(screen.context).apply {
key = hidePaidChaptersPrefKey
title = intl["pref_hide_paid_chapters_title"]
summary = intl["pref_hide_paid_chapters_summary"]
setDefaultValue(true)
}.also(screen::addPreference)
}
fun getHidePaidChaptersPref(preferences: SharedPreferences) = preferences.getBoolean(hidePaidChaptersPrefKey, true)
fun getChapterListSelectorBasedOnHidePaidChaptersPref(baseChapterListSelector: String, preferences: SharedPreferences): String {
if (!getHidePaidChaptersPref(preferences)) {
return baseChapterListSelector
}
// Fragile
val selectors = baseChapterListSelector.split(", ")
return selectors
.map { "$it:not($lockedChapterSelector):not(:has($lockedChapterSelector))" }
.joinToString()
}
}

View File

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

View File

@ -67,6 +67,8 @@ constructor(
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")

View File

@ -1,428 +0,0 @@
package eu.kanade.tachiyomi.multisrc.nepnep
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Source responds to requests with their full database as a JsonArray, then sorts/filters it client-side
* We'll take the database on first requests, then do what we want with it
*/
abstract class NepNep(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/77.0")
private val json: Json by injectLazy()
private lateinit var directory: List<JsonElement>
// Convenience functions to shorten later code
/** Returns value corresponding to given key as a string, or null */
private fun JsonElement.getString(key: String): String? {
return this.jsonObject[key]!!.jsonPrimitive.contentOrNull
}
/** Returns value corresponding to given key as a JsonArray */
private fun JsonElement.getArray(key: String): JsonArray {
return this.jsonObject[key]!!.jsonArray
}
// Popular
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/search/", headers)
}
// don't use ";" for substringBefore() !
private fun directoryFromDocument(document: Document): JsonArray {
val str = document.select("script:containsData(MainFunction)").first()!!.data()
.substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim()
.replace(";", " ")
return json.parseToJsonElement(str).jsonArray
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
thumbnailUrl = document.select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
directory = directoryFromDocument(document).sortedByDescending { it.getString("v") }
return parseDirectory(1)
}
private fun parseDirectory(page: Int): MangasPage {
val mangas = mutableListOf<SManga>()
val endRange = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
for (i in (((page - 1) * 24)..endRange)) {
mangas.add(
SManga.create().apply {
title = directory[i].getString("s")!!
url = "/manga/${directory[i].getString("i")}"
thumbnail_url = getThumbnailUrl(directory[i].getString("i")!!)
},
)
}
return MangasPage(mangas, endRange < directory.lastIndex)
}
private var thumbnailUrl: String? = null
private fun getThumbnailUrl(id: String): String {
if (thumbnailUrl.isNullOrEmpty()) {
val response = client.newCall(popularMangaRequest(1)).execute()
thumbnailUrl = response.asJsoup().select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
}
return thumbnailUrl!!.replace("{{Result.i}}", id)
}
// Latest
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(1)
override fun latestUpdatesParse(response: Response): MangasPage {
directory = directoryFromDocument(response.asJsoup()).sortedByDescending { it.getString("lt") }
return parseDirectory(1)
}
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (page == 1) {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query, filters)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(1)
private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage {
val trimmedQuery = query.trim()
directory = directoryFromDocument(response.asJsoup())
.filter {
// Comparing query with display name
it.getString("s")!!.contains(trimmedQuery, ignoreCase = true) or
// Comparing query with list of alternate names
it.getArray("al").any { altName ->
altName.jsonPrimitive.content.contains(trimmedQuery, ignoreCase = true)
}
}
val genres = mutableListOf<String>()
val genresNo = mutableListOf<String>()
var sortBy: String
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
when (filter) {
is Sort -> {
sortBy = when (filter.state?.index) {
1 -> "ls"
2 -> "v"
else -> "s"
}
directory = if (filter.state?.ascending != true) {
directory.sortedByDescending { it.getString(sortBy) }
} else {
directory.sortedByDescending { it.getString(sortBy) }.reversed()
}
}
is SelectField -> if (filter.state != 0) {
directory = when (filter.name) {
"Scan Status" -> directory.filter { it.getString("ss")!!.contains(filter.values[filter.state], ignoreCase = true) }
"Publish Status" -> directory.filter { it.getString("ps")!!.contains(filter.values[filter.state], ignoreCase = true) }
"Type" -> directory.filter { it.getString("t")!!.contains(filter.values[filter.state], ignoreCase = true) }
"Translation" -> directory.filter { it.getString("o")!!.contains("yes", ignoreCase = true) }
else -> directory
}
}
is YearField -> if (filter.state.isNotEmpty()) directory = directory.filter { it.getString("y")!!.contains(filter.state) }
is AuthorField -> if (filter.state.isNotEmpty()) {
directory = directory.filter { e ->
e.getArray("a").any {
it.jsonPrimitive.content.contains(filter.state, ignoreCase = true)
}
}
}
is GenreList -> filter.state.forEach { genre ->
when (genre.state) {
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
}
}
else -> continue
}
}
if (genres.isNotEmpty()) {
genres.map { genre ->
directory = directory.filter { e ->
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
}
}
}
if (genresNo.isNotEmpty()) {
genresNo.map { genre ->
directory = directory.filterNot { e ->
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
}
}
}
return parseDirectory(1)
}
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
// Details
override fun mangaDetailsParse(response: Response): SManga {
return response.asJsoup().select("div.BoxBody > div.row").let { info ->
SManga.create().apply {
title = info.select("h1").text()
author = info.select("li.list-group-item:has(span:contains(Author)) a").first()?.text()
status = info.select("li.list-group-item:has(span:contains(Status)) a:contains(scan)").text().toStatus()
description = info.select("div.Content").text()
thumbnail_url = info.select("img").attr("abs:src")
val genres = info.select("li.list-group-item:has(span:contains(Genre)) a")
.map { element -> element.text() }
.toMutableSet()
// add series type(manga/manhwa/manhua/other) thinggy to genre
info.select("li.list-group-item:has(span:contains(Type)) a, a[href*=type\\=]").firstOrNull()?.ownText()?.let {
if (it.isEmpty().not()) {
genres.add(it)
}
}
genre = genres.toList().joinToString(", ")
// add alternative name to manga description
val altName = "Alternative Name: "
info.select("li.list-group-item:has(span:contains(Alter))").firstOrNull()?.ownText()?.let {
if (it.isBlank().not() && it != "N/A") {
description = when {
description.isNullOrBlank() -> altName + it
else -> description + "\n\n$altName" + it
}
}
}
}
}
}
private fun String.toStatus() = when {
this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED
this.contains("Cancelled", ignoreCase = true) -> SManga.CANCELLED
this.contains("Hiatus", ignoreCase = true) -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
// Chapters - Mind special cases like decimal chapters (e.g. One Punch Man) and manga with seasons (e.g. The Gamer)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS Z", Locale.getDefault())
private fun chapterURLEncode(e: String): String {
var index = ""
val t = e.substring(0, 1).toInt()
if (1 != t) { index = "-index-$t" }
val dgt = if (e.toInt() < 100100) { 4 } else if (e.toInt() < 101000) { 3 } else if (e.toInt() < 110000) { 2 } else { 1 }
val n = e.substring(dgt, e.length - 1)
var suffix = ""
val path = e.substring(e.length - 1).toInt()
if (0 != path) { suffix = ".$path" }
return "-chapter-$n$suffix$index.html"
}
private val chapterImageRegex = Regex("""^0+""")
private fun chapterImage(e: String, cleanString: Boolean = false): String {
// cleanString will result in an empty string if chapter number is 0, hence the else if below
val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it }
// If b is not zero, indicates chapter has decimal numbering
val b = e.substring(e.length - 1).toInt()
return if (b == 0 && a.isNotEmpty()) {
a
} else if (b == 0 && a.isEmpty()) {
"0"
} else {
"$a.$b"
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val vmChapters = response.asJsoup().select("script:containsData(MainFunction)").first()!!.data()
.substringAfter("vm.Chapters = ").substringBefore(";")
val array = json.parseToJsonElement(vmChapters).jsonArray
val hasDistinctTypes = array.map { it.getString("Type") }.distinct().count() > 1
return array.map { json ->
val indexChapter = json.getString("Chapter")!!
val type = json.getString("Type")
SChapter.create().apply {
name = json.getString("ChapterName").let { if (it.isNullOrEmpty()) "$type ${chapterImage(indexChapter, true)}" else it }
url = "/read-online/" + response.request.url.toString().substringAfter("/manga/") + chapterURLEncode(indexChapter)
// only add type info as scanlator if there are differing types among chapter array
scanlator = if (hasDistinctTypes) type else null
date_upload = try {
json.getString("Date").let { dateFormat.parse("$it +0600")?.time } ?: 0
} catch (_: Exception) {
0L
}
}
}
}
// Pages
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val script = document.selectFirst("script:containsData(MainFunction)")?.data()
?: client.newCall(GET(document.location().removeSuffix(".html"), headers))
.execute().asJsoup().selectFirst("script:containsData(MainFunction)")!!.data()
val curChapter = json.parseToJsonElement(script!!.substringAfter("vm.CurChapter = ").substringBefore(";")).jsonObject
val pageTotal = curChapter.getString("Page")!!.toInt()
val host = "https://" +
script
.substringAfter("vm.CurPathName = \"", "")
.substringBefore("\"")
.also {
if (it.isEmpty()) {
throw Exception("$name is overloaded and blocking Tachiyomi right now. Wait for unblock.")
}
}
val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"")
val seasonURI = curChapter.getString("Directory")!!
.let { if (it.isEmpty()) "" else "$it/" }
val path = "$host/manga/$titleURI/$seasonURI"
val chNum = chapterImage(curChapter.getString("Chapter")!!)
return IntRange(1, pageTotal).mapIndexed { i, _ ->
val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) }
Page(i, "", "$path$chNum-$imageNum.png")
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
// Filters
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Selection(2, false))
private class Genre(name: String) : Filter.TriState(name)
private class YearField : Filter.Text("Years")
private class AuthorField : Filter.Text("Author")
private class SelectField(name: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList(
YearField(),
AuthorField(),
SelectField("Scan Status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
SelectField("Publish Status", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
SelectField("Type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
SelectField("Translation", arrayOf("Any", "Official Only")),
Sort(),
GenreList(getGenreList()),
)
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
// https://manga4life.com/advanced-search/
private fun getGenreList() = listOf(
Genre("Action"),
Genre("Adult"),
Genre("Adventure"),
Genre("Comedy"),
Genre("Doujinshi"),
Genre("Drama"),
Genre("Ecchi"),
Genre("Fantasy"),
Genre("Gender Bender"),
Genre("Harem"),
Genre("Hentai"),
Genre("Historical"),
Genre("Horror"),
Genre("Isekai"),
Genre("Josei"),
Genre("Lolicon"),
Genre("Martial Arts"),
Genre("Mature"),
Genre("Mecha"),
Genre("Mystery"),
Genre("Psychological"),
Genre("Romance"),
Genre("School Life"),
Genre("Sci-fi"),
Genre("Seinen"),
Genre("Shotacon"),
Genre("Shoujo"),
Genre("Shoujo Ai"),
Genre("Shounen"),
Genre("Shounen Ai"),
Genre("Slice of Life"),
Genre("Smut"),
Genre("Sports"),
Genre("Supernatural"),
Genre("Tragedy"),
Genre("Yaoi"),
Genre("Yuri"),
)
}

View File

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

View File

@ -29,6 +29,8 @@ abstract class PizzaReader(
override val supportsLatest = true
override val client = network.cloudflareClient
open val apiUrl by lazy { "$baseUrl$apiPath" }
protected open val json: Json by injectLazy()

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.slimeread.SlimeReadUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadThemeUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -12,10 +12,10 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="slimeread.com"
android:pathPattern="/manga/..*"
android:scheme="https" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.extension.pt.slimeread
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
object SlimeReadFilters {
object SlimeReadThemeFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.slimeread
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import android.app.Activity
import android.content.ActivityNotFoundException
@ -11,7 +11,7 @@ import kotlin.system.exitProcess
* Springboard that accepts https://slimeread.com/manga/<id>/<slug> intents
* and redirects them to the main Tachiyomi process.
*/
class SlimeReadUrlActivity : Activity() {
class SlimeReadThemeUrlActivity : Activity() {
private val tag = javaClass.simpleName
@ -22,7 +22,7 @@ class SlimeReadUrlActivity : Activity() {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${SlimeRead.PREFIX_SEARCH}$item")
putExtra("query", "${SlimeReadTheme.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}

View File

@ -47,6 +47,7 @@ data class MangaInfoDto(
@Serializable
data class ChapterDto(
@SerialName("btc_cap") val number: Float,
@SerialName("btc_date_updated") val updated_at: String,
val scan: ScanDto?,
) {
@Serializable

View File

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

View File

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

View File

@ -27,6 +27,8 @@ abstract class ZeistManga(
override val supportsLatest = true
override val client = network.cloudflareClient
protected val json: Json by injectLazy()
private val intl by lazy { ZeistMangaIntl(lang) }

View File

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

View File

@ -27,7 +27,6 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
@ -42,7 +41,6 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
open class BatoTo(
final override val lang: String,
@ -51,10 +49,11 @@ open class BatoTo(
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrateMirrorPref()
}
override val name: String = "Bato.to"
override val baseUrl: String by lazy { getMirrorPref()!! }
override val baseUrl: String get() = mirror
override val id: Long = when (lang) {
"zh-Hans" -> 2818874445640189582
"zh-Hant" -> 38886079663327225
@ -70,12 +69,9 @@ open class BatoTo(
entryValues = MIRROR_PREF_ENTRY_VALUES
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit()
mirror = newValue as String
true
}
}
val altChapterListPref = CheckBoxPreference(screen.context).apply {
@ -83,11 +79,6 @@ open class BatoTo(
title = ALT_CHAPTER_LIST_PREF_TITLE
summary = ALT_CHAPTER_LIST_PREF_SUMMARY
setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
}
}
val removeOfficialPref = CheckBoxPreference(screen.context).apply {
key = "${REMOVE_TITLE_VERSION_PREF}_$lang"
@ -103,18 +94,35 @@ open class BatoTo(
screen.addPreference(removeOfficialPref)
}
private var mirror = ""
get() {
val current = field
if (current.isNotEmpty()) {
return current
}
field = getMirrorPref()!!
return field
}
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
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)
}
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
val selectedMirror = getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)!!
if (selectedMirror in DEPRECATED_MIRRORS) {
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
}
return this
}
override val supportsLatest = true
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
override val client = network.cloudflareClient
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page", headers)
@ -324,17 +332,28 @@ open class BatoTo(
return super.mangaDetailsRequest(manga)
}
private var titleRegex: Regex =
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)")
Regex("\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|\\/Official|\\/ Official", RegexOption.IGNORE_CASE)
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#mainer div.container-fluid")
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 originalTitle = infoElement.select("h3").text().removeEntities()
val alternativeTitles = document.select("div.pb-2.alias-set.line-b-f").text()
val description = infoElement.select("div.limit-html").text() + "\n" +
infoElement.select(".episode-list > .alert-warning").text().trim()
val description = buildString {
append(infoElement.select("div.limit-html").text())
infoElement.selectFirst(".episode-list > .alert-warning")?.also {
append("\n\n${it.text()}")
}
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
append("\n\nExtra Info:\n${it.text()}")
}
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
append("\n\nAlternative Titles:\n")
append(it.text().split('/').joinToString("\n") { "${it.trim()}" })
}
}
val cleanedTitle = if (isRemoveTitleVersion()) {
originalTitle.replace(titleRegex, "").trim()
} else {
@ -346,8 +365,7 @@ open class BatoTo(
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
manga.status = parseStatus(workStatus, uploadStatus)
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
manga.description = description +
if (alternativeTitles.isNotBlank()) "\n\nAlternative Titles:\n$alternativeTitles" else ""
manga.description = description
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
return manga
}
@ -983,7 +1001,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(
"bato.to",
"zbato.org",
"batocomic.com",
"batocomic.net",
"batocomic.org",
@ -992,9 +1010,6 @@ open class BatoTo(
"battwo.com",
"comiko.net",
"comiko.org",
"mangatoto.com",
"mangatoto.net",
"mangatoto.org",
"readtoto.com",
"readtoto.net",
"readtoto.org",
@ -1009,11 +1024,17 @@ open class BatoTo(
"xbato.org",
"zbato.com",
"zbato.net",
"zbato.org",
)
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://mangatoto.com",
"https://mangatoto.net",
"https://mangatoto.org",
)
private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST"
private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List"
private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list"

View File

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

View File

@ -177,7 +177,7 @@ abstract class Comick(
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
}
override val client = network.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.addNetworkInterceptor(::errorInterceptor)
.rateLimit(3, 1, TimeUnit.SECONDS)
.build()

View File

@ -1,7 +1,7 @@
ext {
extName = 'Cubari'
extClass = '.CubariFactory'
extVersionCode = 24
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"

View File

@ -38,7 +38,7 @@ open class Cubari(override val lang: String) : HttpSource() {
private val json: Json by injectLazy()
override val client = super.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val headers = request.headers.newBuilder()

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.deviantart.DeviantArtUrlActivity"
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="http"/>
<data android:scheme="https"/>
<data android:host="www.deviantart.com"/>
<data android:host="deviantart.com"/>
<data android:pathPattern="/..*/gallery/..*"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,173 @@
package eu.kanade.tachiyomi.extension.all.deviantart
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class DeviantArt : HttpSource() {
override val name = "DeviantArt"
override val baseUrl = "https://deviantart.com"
override val lang = "all"
override val supportsLatest = false
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")
}
private val backendBaseUrl = "https://backend.deviantart.com"
private fun backendBuilder() = backendBaseUrl.toHttpUrl().newBuilder()
private val dateFormat by lazy {
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
}
private fun parseDate(dateStr: String?): Long {
return try {
dateFormat.parse(dateStr ?: "")!!.time
} catch (_: ParseException) {
0L
}
}
override fun popularMangaRequest(page: Int): Request {
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
}
override fun popularMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val matchGroups = requireNotNull(
Regex("""gallery:([\w-]+)(?:/(\d+))?""").matchEntire(query)?.groupValues,
) { SEARCH_FORMAT_MSG }
val username = matchGroups[1]
val folderId = matchGroups[2].ifEmpty { "all" }
return GET("$baseUrl/$username/gallery/$folderId", headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val manga = mangaDetailsParse(response)
return MangasPage(listOf(manga), false)
}
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesParse(response: Response): MangasPage {
throw UnsupportedOperationException()
}
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()
author = document.title().substringBefore(" ")
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = subFolderGallery?.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 url = backendBuilder()
.addPathSegment("rss.xml")
.addQueryParameter("q", query)
.build()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoupXml()
val chapterList = parseToChapterList(document).toMutableList()
var nextUrl = document.selectFirst("[rel=next]")?.absUrl("href")
while (nextUrl != null) {
val newRequest = GET(nextUrl, headers)
val newResponse = client.newCall(newRequest).execute()
val newDocument = newResponse.asJsoupXml()
val newChapterList = parseToChapterList(newDocument)
chapterList.addAll(newChapterList)
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
}
return indexChapterList(chapterList.toList())
}
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 {
name = it.selectFirst("title")!!.text()
date_upload = parseDate(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text()
}
}
}
private fun indexChapterList(chapterList: List<SChapter>): 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() }
}
} else {
chapterList.mapIndexed { i, chapter ->
chapter.apply { 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))
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
private fun Response.asJsoupXml(): Document {
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
}
companion object {
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.snowmtl
package eu.kanade.tachiyomi.extension.all.deviantart
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,28 +7,28 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class SnowmtlUrlActivity : Activity() {
private val tag = javaClass.simpleName
class DeviantArtUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
if (pathSegments != null && pathSegments.size >= 3) {
val username = pathSegments[0]
val folderId = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Snowmtl.PREFIX_SEARCH}$item")
putExtra("query", "gallery:$username/$folderId")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
Log.e("DeviantArtUrlActivity", e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
Log.e("DeviantArtUrlActivity", "Could not parse URI from intent $intent")
}
finish()

View File

@ -1,7 +1,7 @@
ext {
extName = 'E-Hentai'
extClass = '.EHFactory'
extVersionCode = 22
extVersionCode = 24
isNsfw = true
}

View File

@ -48,11 +48,13 @@ abstract class EHentai(
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
private val memberId: String by lazy { getMemberIdPref() }
private val passHash: String by lazy { getPassHashPref() }
private val igneous: String by lazy { getIgneousPref() }
private val forceEh: Boolean by lazy { getForceEhPref() }
override val baseUrl: String
get() = when {
System.getenv("CI") == "true" -> "https://e-hentai.org"
memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
!forceEh && memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
else -> "https://e-hentai.org"
}
@ -170,18 +172,18 @@ abstract class EHentai(
query.isBlank() -> languageTag(enforceLanguageFilter)
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
}
filters.filterIsInstance<TextFilter>().forEach { it ->
if (it.state.isNotEmpty()) {
val splitted = it.state.split(",").filter(String::isNotBlank)
if (splitted.size < 2 && it.type != "tags") {
modifiedQuery += " ${it.type}:\"${it.state.replace(" ", "+")}\""
filters.filterIsInstance<TextFilter>().forEach { filter ->
if (filter.state.isNotEmpty()) {
val splitted = filter.state.split(",").filter(String::isNotBlank)
if (splitted.size < 2 && filter.type != "tags") {
modifiedQuery += " ${filter.type}:\"${filter.state.replace(" ", "+")}\""
} else {
splitted.forEach { tag ->
val trimmed = tag.trim().lowercase()
if (trimmed.startsWith('-')) {
modifiedQuery += " -${it.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
modifiedQuery += if (trimmed.startsWith('-')) {
" -${filter.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
} else {
modifiedQuery += " ${it.type}:\"${trimmed.replace(" ", "+")}\""
" ${filter.type}:\"${trimmed.replace(" ", "+")}\""
}
}
}
@ -378,7 +380,7 @@ abstract class EHentai(
cookies["ipb_pass_hash"] = passHash
cookies["igneous"] = ""
cookies["igneous"] = igneous
buildCookies(cookies)
}
@ -398,7 +400,7 @@ abstract class EHentai(
.appendQueryParameter(param, value)
.toString()
override val client = network.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.cookieJar(CookieJar.NO_COOKIES)
.addInterceptor { chain ->
val newReq = chain
@ -414,6 +416,7 @@ abstract class EHentai(
// Filters
override fun getFilterList() = FilterList(
EnforceLanguageFilter(getEnforceLanguagePref()),
Favorites(),
Watched(),
GenreGroup(),
Filter.Header("Separate tags with commas (,)"),
@ -435,6 +438,14 @@ abstract class EHentai(
}
}
class Favorites : CheckBox("Favorites"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state) {
builder.appendPath("favorites.php")
}
}
}
class GenreOption(name: String, private val genreId: String) : CheckBox(name, false), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
@ -561,21 +572,33 @@ abstract class EHentai(
private const val PASS_HASH_PREF_TITLE = "ipb_pass_hash"
private const val PASS_HASH_PREF_SUMMARY = "ipb_pass_hash value"
private const val PASS_HASH_PREF_DEFAULT_VALUE = ""
private const val IGNEOUS_PREF_KEY = "IGNEOUS"
private const val IGNEOUS_PREF_TITLE = "igneous"
private const val IGNEOUS_PREF_SUMMARY = "igneous value override"
private const val IGNEOUS_PREF_DEFAULT_VALUE = ""
private const val FORCE_EH = "FORCE_EH"
private const val FORCE_EH_TITLE = "Force e-hentai"
private const val FORCE_EH_SUMMARY = "Force e-hentai to avoid content on exhentai"
private const val FORCE_EH_DEFAULT_VALUE = true
}
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val forceEhPref = CheckBoxPreference(screen.context).apply {
key = FORCE_EH
title = FORCE_EH_TITLE
summary = FORCE_EH_SUMMARY
setDefaultValue(FORCE_EH_DEFAULT_VALUE)
}
val enforceLanguagePref = CheckBoxPreference(screen.context).apply {
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
title = ENFORCE_LANGUAGE_PREF_TITLE
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
}
}
val memberIdPref = EditTextPreference(screen.context).apply {
@ -593,8 +616,19 @@ abstract class EHentai(
setDefaultValue(PASS_HASH_PREF_DEFAULT_VALUE)
}
val igneousPref = EditTextPreference(screen.context).apply {
key = IGNEOUS_PREF_KEY
title = IGNEOUS_PREF_TITLE
summary = IGNEOUS_PREF_SUMMARY
setDefaultValue(IGNEOUS_PREF_DEFAULT_VALUE)
}
screen.addPreference(forceEhPref)
screen.addPreference(memberIdPref)
screen.addPreference(passHashPref)
screen.addPreference(igneousPref)
screen.addPreference(enforceLanguagePref)
}
@ -629,4 +663,12 @@ abstract class EHentai(
private fun getMemberIdPref(): String {
return getCookieValue(MEMBER_ID_PREF_TITLE, MEMBER_ID_PREF_DEFAULT_VALUE, MEMBER_ID_PREF_KEY)
}
private fun getIgneousPref(): String {
return getCookieValue(IGNEOUS_PREF_TITLE, IGNEOUS_PREF_DEFAULT_VALUE, IGNEOUS_PREF_KEY)
}
private fun getForceEhPref(): Boolean {
return preferences.getBoolean(FORCE_EH, FORCE_EH_DEFAULT_VALUE)
}
}

View File

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

View File

@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
import eu.kanade.tachiyomi.multisrc.mangaesp.TopSeriesDto
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import okhttp3.FormBody
@ -27,21 +24,7 @@ open class EternalMangas(
"https://eternalmangas.com",
lang,
) {
override fun popularMangaParse(response: Response): MangasPage {
val body = response.body.string()
val responseData = json.decodeFromString<TopSeriesDto>(body)
val topDaily = responseData.response.topDaily.flatten().map { it.data }
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
.filter { it.language == internalLang }
.map { it.toSManga(seriesPath) }
return MangasPage(mangas, false)
}
override val useApiSearch = true
override fun latestUpdatesParse(response: Response): MangasPage {
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
@ -49,16 +32,8 @@ open class EternalMangas(
return MangasPage(mangas, false)
}
override fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
val document = response.asJsoup()
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson)
.filter { it.language == internalLang }
.toMutableList()
return parseComicsList(page, query, filters)
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
return this.filter { it.language == internalLang }.toMutableList()
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {

View File

@ -1,10 +1,22 @@
package eu.kanade.tachiyomi.extension.all.hentaifox
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
import eu.kanade.tachiyomi.multisrc.galleryadults.SortOrderFilter
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
import eu.kanade.tachiyomi.multisrc.galleryadults.toDate
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class HentaiFox(
@ -100,4 +112,103 @@ class HentaiFox(
Filter.Header("HINT: Use double quote (\") for exact match"),
) + super.getFilterList().list,
)
private val sidebarPath = "includes/sidebar.php"
private fun sidebarMangaSelector() = "div.item"
private fun Element.sidebarMangaTitle() =
selectFirst("img")?.attr("alt")
private fun Element.sidebarMangaUrl() =
selectFirst("a")?.attr("abs:href")
private fun Element.sidebarMangaThumbnail() =
selectFirst("img")?.imgAttr()
private var csrfToken: String? = null
override fun tagsParser(document: Document): List<Genre> {
csrfToken = csrfParser(document)
return super.tagsParser(document)
}
private fun csrfParser(document: Document): String {
return document.select("[name=csrf-token]").attr("content")
}
private fun setSidebarHeaders(csrfToken: String?): Headers {
if (csrfToken == null) {
return xhrHeaders
}
return xhrHeaders.newBuilder()
.add("X-Csrf-Token", csrfToken)
.build()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Sidebar mangas should always override any other search, so they should appear first
// and only propagate to super when a "normal" search is issued
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
sortOrderFilter?.let {
val selectedCategory = sortOrderFilter.values.get(sortOrderFilter.state)
if (sidebarCategoriesFilterStateMap.containsKey(selectedCategory)) {
return sidebarRequest(
sidebarCategoriesFilterStateMap.getValue(selectedCategory),
)
}
}
return super.searchMangaRequest(page, query, filters)
}
private fun sidebarRequest(category: String): Request {
val url = "$baseUrl/$sidebarPath"
return POST(
url,
setSidebarHeaders(csrfToken),
FormBody.Builder()
.add("type", category)
.build(),
)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.encodedPath.endsWith(sidebarPath)) {
val document = response.asJsoup()
val mangas = document.select(sidebarMangaSelector())
.map {
SMangaDto(
title = it.sidebarMangaTitle()!!,
url = it.sidebarMangaUrl()!!,
thumbnail = it.sidebarMangaThumbnail(),
lang = LANGUAGE_MULTI,
)
}
.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.thumbnail
}
}
return MangasPage(mangas, false)
} else {
return super.searchMangaParse(response)
}
}
override fun getSortOrderURIs(): List<Pair<String, String>> {
return super.getSortOrderURIs() + sidebarCategoriesFilterStateMap.toList()
}
companion object {
private val sidebarCategoriesFilterStateMap = mapOf(
"Top Rated" to "top_rated",
"Most Faved" to "top_faved",
"Most Fapped" to "top_fapped",
"Most Downloaded" to "top_downloaded",
).withDefault { "top_rated" }
}
}

View File

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

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
@ -29,6 +30,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.http2.StreamResetException
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -42,6 +44,7 @@ import java.util.LinkedList
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalUnsignedTypes::class)
class Hitomi(
@ -62,7 +65,10 @@ class Hitomi(
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::Intercept)
.addInterceptor(::jxlContentTypeInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
.build()
private val preferences: SharedPreferences by lazy {
@ -708,7 +714,7 @@ class Hitomi(
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
}
private fun Intercept(chain: Interceptor.Chain): Response {
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headers["Content-Type"] != "application/octet-stream") {
return response
@ -728,6 +734,20 @@ class Hitomi(
.build()
}
private fun streamResetRetry(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: StreamResetException) {
Log.e(name, "reset", e)
if (e.message.orEmpty().contains("INTERNAL_ERROR")) {
Thread.sleep(2.seconds.inWholeMilliseconds)
chain.proceed(chain.request())
} else {
throw e
}
}
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()

View File

@ -22,9 +22,9 @@ class Gallery(
@Serializable
class ImageFile(
val hash: String,
val haswebp: Int,
val hasavif: Int,
val hasjxl: Int,
val haswebp: Int?,
val hasavif: Int?,
val hasjxl: Int?,
)
@Serializable

View File

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

View File

@ -156,6 +156,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
1 -> if (type == "series") "metadata.titleSort" else "name"
2 -> "createdDate"
3 -> "lastModifiedDate"
4 -> "random"
else -> return@forEach
} + "," + if (state.ascending) "asc" else "desc"

View File

@ -19,7 +19,7 @@ internal class TypeSelect : Filter.Select<String>(
internal class SeriesSort(selection: Selection? = null) : Filter.Sort(
"Sort",
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated"),
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated", "Random"),
selection ?: Selection(0, false),
)

View File

@ -33,7 +33,7 @@ open class MangaFire(
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor)
.build()

View File

@ -31,7 +31,7 @@ open class MangaReader(
override val baseUrl = "https://mangareader.to"
override val client = network.client.newBuilder()
override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor)
.build()

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ class Mitaku : ParsedHttpSource() {
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/ero-cosplay/page/$page", headers)
override fun popularMangaSelector() = "div.article-container article"
override fun popularMangaSelector() = "div.cm-primary article"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))

View File

@ -1,7 +1,7 @@
ext {
extName = 'MyReadingManga'
extClass = '.MyReadingMangaFactory'
extVersionCode = 53
extVersionCode = 56
isNsfw = true
}

View File

@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
@ -29,10 +28,21 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
// Basic Info
override val name = "MyReadingManga"
final override val baseUrl = "https://myreadingmanga.info"
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder =
super.headersBuilder()
.set("User-Agent", USER_AGENT)
.add("X-Requested-With", randomString((1..20).random()))
override val client = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val headers = request.headers.newBuilder().apply {
removeAll("X-Requested-With")
}.build()
chain.proceed(request.newBuilder().headers(headers).build())
}
.build()
override val supportsLatest = true
// Popular - Random
@ -308,7 +318,16 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
Filter.Select<String>(displayName, vals.map { it }.toTypedArray(), defaultValue), UriFilter {
override fun addToUri(uri: Uri.Builder, uriParam: String) {
if (state != 0 || !firstIsUnspecified) {
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
val splitFilter = vals[state].split(",")
when {
splitFilter.size == 2 -> {
val reversedFilter = splitFilter.reversed().joinToString(" | ").trim()
uri.appendQueryParameter(uriParam, "$uriValuePrefix:$reversedFilter")
}
else -> {
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
}
}
}
}
}
@ -321,6 +340,11 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
}
companion object {
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36"
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36"
}
private fun randomString(length: Int): String {
val charPool = ('a'..'z') + ('A'..'Z')
return List(length) { charPool.random() }.joinToString("")
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.namicomi.NamiComiUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter
android:autoVerify="false"
tools:targetApi="23">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="namicomi.com" />
<data android:scheme="https" />
<data android:pathPattern="/.*/title/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,114 @@
content=Content
content_rating=Content rating
content_rating_genre=Content rating: %s
content_rating_mature=Mature
content_rating_restricted=Restricted
content_rating_safe=Safe
content_warnings_drugs=Drugs
content_warnings_gambling=Gambling
content_warnings_gore=Gore
content_warnings_mental_disorders=Mental Disorders
content_warnings_physical_abuse=Physical Abuse
content_warnings_racism=Racism
content_warnings_self_harm=Self-harm
content_warnings_sexual_abuse=Sexual Abuse
content_warnings_verbal_abuse=Verbal Abuse
cover_quality=Cover quality
cover_quality_low=Low
cover_quality_medium=Medium
cover_quality_original=Original
data_saver=Data saver
data_saver_summary=Enables smaller, more compressed images
error_payment_required=Payment required. Chapter requires a premium subscription
excluded_tags_mode=Excluded tags mode
format=Format
format_4_koma=4-Koma
format_adaptation=Adaptation
format_anthology=Anthology
format_full_color=Full Color
format_oneshot=Oneshot
format_silent=Silent
genre=Genre
genre_action=Action
genre_adventure=Adventure
genre_boys_love=Boys' Love
genre_comedy=Comedy
genre_crime=Crime
genre_drama=Drama
genre_fantasy=Fantasy
genre_girls_love=Girls' Love
genre_historical=Historical
genre_horror=Horror
genre_isekai=Isekai
genre_mecha=Mecha
genre_medical=Medical
genre_mystery=Mystery
genre_philosophical=Philosophical
genre_psychological=Psychological
genre_romance=Romance
genre_sci_fi=Sci-Fi
genre_slice_of_life=Slice of Life
genre_sports=Sports
genre_superhero=Superhero
genre_thriller=Thriller
genre_tragedy=Tragedy
genre_wuxia=Wuxia
has_available_chapters=Has available chapters
included_tags_mode=Included tags mode
invalid_manga_id=Not a valid title ID
mode_and=And
mode_or=Or
show_locked_chapters=Show locked/paywalled chapters
show_locked_chapters_summary=Display chapters that require an account with a premium subscription
sort=Sort
sort_alphabetic=Alphabetic
sort_content_created_at=Content created at
sort_number_of_chapters=Chapter count
sort_number_of_comments=Comment count
sort_number_of_follows=Followers
sort_number_of_likes=Likes
sort_rating=Rating
sort_views=Views
sort_year=Year
status=Status
status_cancelled=Cancelled
status_completed=Completed
status_hiatus=Hiatus
status_ongoing=Ongoing
tags_mode=Tags mode
theme=Theme
theme_aliens=Aliens
theme_animals=Animals
theme_cooking=Cooking
theme_crossdressing=Crossdressing
theme_delinquents=Delinquents
theme_demons=Demons
theme_genderswap=Genderswap
theme_ghosts=Ghosts
theme_gyaru=Gyaru
theme_harem=Harem
theme_mafia=Mafia
theme_magic=Magic
theme_magical_girls=Magical Girls
theme_martial_arts=Martial Arts
theme_military=Military
theme_monster_girls=Monster Girls
theme_monsters=Monsters
theme_music=Music
theme_ninja=Ninja
theme_office_workers=Office Workers
theme_police=Police
theme_post_apocalyptic=Post-Apocalyptic
theme_reincarnation=Reincarnation
theme_reverse_harem=Reverse Harem
theme_samurai=Samurai
theme_school_life=School Life
theme_supernatural=Supernatural
theme_survival=Survival
theme_time_travel=Time Travel
theme_traditional_games=Traditional Games
theme_vampires=Vampires
theme_video_games=Video Games
theme_villainess=Villainess
theme_virtual_reality=Virtual Reality
theme_zombies=Zombies

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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