Compare commits

...

107 Commits

Author SHA1 Message Date
Chopper
07eba2f8f2
Keyoapp: Add new variant to popularManga selector (#8206)
All checks were successful
CI / Prepare job (push) Successful in 7s
CI / Build individual modules (push) Successful in 9m54s
CI / Publish repo (push) Successful in 50s
Add new variant to popularManga selector
2025-03-27 00:02:40 +00:00
AwkwardPeak7
77bd833e6a
Hitomi: change cdn domain & fix image url logic (#8204)
* Hitomi: change cdn domain & image url logic

* remove unused

* avifbigtn
2025-03-27 00:02:40 +00:00
wasu
ea28acd641
Add OhJoySexToy (#8149)
* Add OhJoySexToy

* better CSS selectors

* shorten description

Description format vary per comic, some have one paragraph, others many, some include transcript, links or even more advanced HTML structures

* override title in mangaDetailsParse

* fix: wrong date element

* use absolute urls

* rename file (fix doubled file extension)

* move SimpleDateFormat to class
2025-03-27 00:02:40 +00:00
Chopper
a8e99cea12
Nartag: Update domain (#8194)
Update domain
2025-03-27 00:02:36 +00:00
theAutotelicX
e0193a707a
Add Moodtoon (#8181) 2025-03-27 00:02:36 +00:00
Vetle Ledaal
3fb70c10de
Koharu: fix DTO & bypass Cloudflare (#8175)
* Koharu - \r\n → \n

* SchaleNetwork: fix loading & support related-manga

* (SchaleNetwok/Koharu): Fix DTO & bypass Cloudflare (#128)

* Fix DTO
* Bypass CloudFlare Turnstile
* Add tags filter select box which supports click on 'tag' to search
* Allow permanent excluded tags

* Revert fork specific Koharu changes

* Bump version + lint

---------

Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
2025-03-27 00:02:35 +00:00
Chopper
414b6b8670
NyxScans: Fix api url (#8172)
Fix api url
2025-03-27 00:02:34 +00:00
Chopper
ee5a221a17
Mangaworld: Update domain (#8171)
Update domain
2025-03-27 00:02:34 +00:00
Vetle Ledaal
d43dd231b2
Update domain for Catharsis World (#8170) 2025-03-27 00:02:34 +00:00
Vetle Ledaal
5b7ce2f85d
Remove broken extensions/sites (#8167)
* Remove HerenScan (403)

* Remove Its Your Right Manhua (403)

* Remove PrinceEdiciones (403)

* Remove Manhwas.es (403)

* Remove Yaoi Manga (403)

* Remove Kings Of Darkness (404)

* Remove ManhwaFreak.xyz (404)

* Remove HenChan (404)

* Remove Yaoi Flix (404)

* Remove Asura Scans.us (unoriginal) (disabled)

* Remove Bento Manga (404)

* Remove MR Yaoi Fansub (expired)

* Remove Aurora Scan (empty)

* Remove Varna Scan (unrelated)

* Remove Guilda Tier Draw (disabled / invite only)

* Remove Norte Rose Scan (under maintenance)

* Remove Nox Scans (disabled)

* Remove Novelle Leggere (unrelated)

* Remove IchiroManga (disabled)

* Remove LeerMangasXYZ (unrelated)

* Remove Wicked Scans (526, invalid SSL certificate)
2025-03-27 00:02:34 +00:00
Vetle Ledaal
af0d261251
Remove extensions without A record (#8165)
* Remove MangaBook

* Remove Wicked Witch Scan
2025-03-27 00:02:34 +00:00
Pandu
7e13a5ca27
Noromax: Update Domain (#8164) 2025-03-27 00:02:34 +00:00
Chopper
3b85cfc5a2
SussyToons: Fix source (#8155)
* Fix popular manga and latest manga

* Fix details and chapter

* Fix search

* Bump version
2025-03-27 00:02:34 +00:00
Vetle Ledaal
44a4f517d2
Remove remaining usage of network.client (#8153)
* Explicitly use `network.cloudflareClient` instead of `super.client`

... but only if the parent is `HttpSource` or `ParsedHttpSource`

* Remove use of `network.client`

... replaced with `network.cloudflareClient` or `super.client` if the
parent class sets `client` to `network.cloudflareClient` already.
2025-03-27 00:02:34 +00:00
Fermín Cirella
63d0d76535
Doujin.io: only use images as pages (#8152) 2025-03-27 00:02:34 +00:00
Uranus
72c0ecc64f
Komga: add search for books (#8102)
* Komga: add search for books

* clean up unused code

* add String.isFromBook() method

* fix lint error
2025-03-27 00:02:34 +00:00
Chopper
91b33530ac
Add some sources (madara) (#8142)
* Add MangaxCore

* Add ClanMarcial

* Add MangaRomange

* Add TresDaosNet

* Add YetiskinRuyaManga

* Omit capture group

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

* Add required login check

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-03-27 00:02:34 +00:00
Chopper
f192dc994e
Add some sources (mangathemesia) (#8141)
* Add MangaTV

* Add MangaOku

* Add RofanToon

* Add SpeedManga

* Add KomikDewasa

* Fix MangaTV dateFormat
2025-03-27 00:02:34 +00:00
Fioren
3bc05706e5
Fix images not load TopTruyen (#8130)
Fix images not load and update domain TopTruyen
2025-03-27 00:02:34 +00:00
Haxel Caribou
3fe012a4c7
Add egscomics (#8118)
* Create extension (copied from gunnerkrigg)

* Add page upload date

* Update icons

* Fix bug where code can fail depending on locale

* Add egsnp and sketchbook

* Implement suggested maintainer edits

Update src/en/egscomics/src/eu/kanade/tachiyomi/extension/en/egscomics/ElGoonishShive.kt

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:34 +00:00
Chopper
f1731b643e
VortexScans: Fix popularManga url (#8136)
Fix popularManga url
2025-03-27 00:02:34 +00:00
Chopper
f692c85526
LilyManga: Add rateLimit (#8129)
* Add rateLimit

* Use rateLimitHost
2025-03-27 00:02:34 +00:00
Chopper
3a8918ae58
OtaScans: Update domain (#8132)
Update domain
2025-03-27 00:02:34 +00:00
Chopper
30a65efcb0
WeebCentral: Fix manga title (#8131)
Fix manga title
2025-03-27 00:02:34 +00:00
Chopper
c5fbede326
Taiyo: Fix token (#8127)
Fix token
2025-03-27 00:02:34 +00:00
Fioren
6f47ed8bc1
Fix images load DocTruyen3Q (#8119) 2025-03-27 00:02:34 +00:00
Creepler13
9dc2d6f03f
Fix Flamecomics Thumbnail (#8117)
* Fix Flamecomics Thumbnail

* wrap thumnail in next_url
2025-03-27 00:02:34 +00:00
AlphaBoom
99f58ad3f1
Yidan: Rewrite and rework the extension. (#8091)
* Yidan: Rewrite and rework the extension.

* Yidan: Remove logs.

* Yidan: Lint?

* Yidan: Apply review suggestions

Co-Authored-By: Vetle Ledaal <13540478+vetleledaal@users.noreply.github.com>

* Apply review suggestions

Co-Authored-By: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: Vetle Ledaal <13540478+vetleledaal@users.noreply.github.com>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:34 +00:00
Creepler13
98f7d48324
Iken: remove false scanlator (#8086)
* Iken Remove scanlator

* remove dto
2025-03-27 00:02:34 +00:00
BrutuZ
8a14edfd48
Comick: Localized title setting (#8111)
* Localized title preference

* lint
2025-03-27 00:02:34 +00:00
Chopper
99c8f52676
Update domains (#8108)
* Update SamuraiScan domain

* Update RuyaManga domain

* Update DuaLeonTruyen domain

* Bump version

* Add interceptor in SamuraiScan
2025-03-27 00:02:34 +00:00
Chopper
2c52f117d4
Remove dead sources (#8099)
* Remove ElevenScanlator

* Remove FlowerMangaDotCom

* Remove InfinyxScan

* Remove LScans

* Remove YaoiComics

* Remove NewManhua

* Remove ManhuaZone

* Revert "Remove InfinyxScan"
2025-03-27 00:02:34 +00:00
Chopper
db2813a793
Add PinkRosa (#8096) 2025-03-27 00:02:34 +00:00
Chopper
74edbbae7d
Add PointZeroToons (#8095) 2025-03-27 00:02:34 +00:00
Chopper
456b3fff5e
Add OsakaScan (#8094)
* Add OsakaScan

* Fix chapter sort

* Fix page loading
2025-03-27 00:02:34 +00:00
Chopper
de293f4685
Add WindScan (#8093)
* Add WindScan

* Add LoadMoreStrategy and chapter endpoint flag
2025-03-27 00:02:34 +00:00
Chopper
ac2ebae360
Add TiaManhwa (#8092)
* Add TiaManhwa

* Add LoadMoreStrategy and chapter endpoint flag
2025-03-27 00:02:34 +00:00
AwkwardPeak7
55e3a5b101
Danbooru: fix app crash
oops

closes #8101
2025-03-27 00:02:31 +00:00
cumberjie
249ea8a3ad
update yidan mirrors web (#8081)
* Update Yidan.kt

* Update build.gradle
2025-03-27 00:02:31 +00:00
Jake
d7c6574e87
MangaHub (multisrc) - Fix "API Limit Exceeded" (#8073)
* Fixed "API limit exceeded"

gonna crash out if this build failed again bc of linting

* review changes

* Review changes
2025-03-27 00:02:31 +00:00
Chopper
34b4284d7c
Add YuYu multsrc (#8080) 2025-03-27 00:02:31 +00:00
Creepler13
a4cd04699c
Fix Baozi (#8078)
Fix baozi
2025-03-27 00:02:31 +00:00
Vetle Ledaal
1c3e40c5dc
Remove extensions with parked domains (#8077)
Remove some parked domains

Fairy Manga
KomikPlay
LegendScanlations
MangaToRead
MangaUS
MangaXP
NeatManga
NOISE Manga
Rainbow Fairy Scan
Sawamics
Zahard
2025-03-27 00:02:31 +00:00
Vetle Ledaal
5508ced068
Remove extensions redirecting to other extensions (#8076)
* Remove Apolltoons (replaced by Mundo Manhwa)

* Remove Arctic Scan (replaced by Yushuke Mangas)

* chore: update comment bacakomik.co -> bacakomik.one

* Remove Black Scans (replaced by Yugen Mangás)

* Remove KomikIndo.info (replaced by Mangasusu)

* Remove Snow Scans (replaced by Galaxy)

* Remove Vex Manga (replaced by Vortex Scans)

* Remove MangaSaki (replaced by Mangasail)

* Remove Xmanhwa (replaced by ManhwaDen)
2025-03-27 00:02:31 +00:00
Vetle Ledaal
f5aecd2ad4
Remove extensions without A record (#8075)
Hreads
Manga 18h
Mangá Ninja
Mangaxico
Mangayaro
Otsugami
ReadComic.Top
Tyrant Scans
Wonderland Scan
2025-03-27 00:02:31 +00:00
Cleopatra
ddf63bf592
Comick: Comick: Added Exclude Tags Filter, Fixed Demographic Type, Added "None" / "Others" Filter, Added Content Rating Filter (#8049)
* Added Exclude Tags Filter

* Added None Demographic & Other Type.

Switched DemographicFilter from TriFilter -> CheckBoxFilter (API Doesn't support it) & switched it.isIncluded() -> it.state

* Added Content Rating Filter

* Added extension setting to hide/show alternative titles.

* Extracted duplicate tags code into a function.
2025-03-27 00:02:31 +00:00
Hellkaros
7154880810
Add MangaMura (#7551)
* Add MangaMura

* refactor: code changes

* fix

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:31 +00:00
AwkwardPeak7
851564a4e2
Danbooru: option to list each post in series as seperate chapter (#8058)
* refctor

* option to split chapter list + dto

* httpsource as other methods were unused

* lint

* chapter order and artist field

* latest post date
2025-03-27 00:02:31 +00:00
Creepler13
31d83bbfc3
Add/Fix Mangalivre + Madara change (#8062)
* Add/Fix Mangalivre + Madara change

* madara version

* add old Mangalivre id
2025-03-27 00:02:31 +00:00
Creepler13
4d5e44c2a6
Add Inventario Oculto (#8061)
add Inventario Oculto
2025-03-27 00:02:31 +00:00
Alan Tan
c986f2924c
Colamanga: add upload date to latest chapter (#7935)
* ColaMange: Update filter

* ColaMange: add date_upload

The mange chapter should used the last updated date as the date_upload for user to better predict the next new chapters date

* ColaMange: Improve date parse

* Revert bump

* ColoManga: minor update

* ColoManga: bump

* ColoManga:  add error check

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:31 +00:00
Smol Ame
d58a274c70
Remove YanpFansub (#8056) 2025-03-27 00:02:31 +00:00
bapeey
577bbb69bf
Inari Manga: Auto update domain (#8051)
autoupdate domain
2025-03-27 00:02:31 +00:00
bapeey
38c5f53e3b
Olympus Scanlation: Update domain and reduce search query length to api limit (#8040)
* cut query

* update domain
2025-03-27 00:02:31 +00:00
Smol Ame
ab5474a92b
Shinigami: Bump extVersionCode (#8043) 2025-03-27 00:02:31 +00:00
Smol Ame
13949bc6f3
Remove Reaper Scans (unoriginal) (#8034) 2025-03-27 00:02:31 +00:00
bapeey
d7f03a9caa
TempleScan(esp): Fix comic data not found (#8030)
* fix no data found

* bump
2025-03-27 00:02:31 +00:00
Chopper
407af100d4
YugenMangas: Fix loading content (#7990)
* Fix loading content

* Remove init and use utils functions
2025-03-27 00:02:31 +00:00
Jake
57e51e8ef1
Rewrite Mangabox (Mangakakalot, Manganato, Mangabat) to Allow Mirrors and CDN Fallbacks (#7915)
* Added CDN Fallback For Mangabox-based extensions

* Improved CDN testing

Now prioritizes last-worked CDNs

Seems like they "fixed" the issue by changing the alternative/backup CDNs to a single, working CDN.

* re-added the removed null check at line 68

* refactored, made fallbacks configurable

* Removed mangairo

* Added mirrors

* lint

* lint again

* final lint

* review changes, lint

* refactor, lint

* lint again 😩
2025-03-27 00:02:31 +00:00
Luqman
ff9732e42b
Shinigami: Rewrite for new site (#7894)
* Shinigami: Rewrite for new site

* add migration info

* tweak baseurl

* use 1 date formatter

* handle decimal chapter number

* Update src/id/shinigami/src/eu/kanade/tachiyomi/extension/id/shinigami/Shinigami.kt

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

* cleaning

* remove unused

* Update src/id/shinigami/src/eu/kanade/tachiyomi/extension/id/shinigami/Shinigami.kt

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

* Apply suggestions from code review

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

* tweak date

Co-Authored-By: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* remove unused

* api version inline

* Apply suggestions from code review

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

* cleaning

* cleaning2

* Update src/id/shinigami/src/eu/kanade/tachiyomi/extension/id/shinigami/Shinigami.kt

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

* remove baseUrl config

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-03-27 00:02:31 +00:00
Smol Ame
5025203010
Remove Sinensis Scan (#8025)
Sinensis Scan merged with Cerise Scan as reported in #7424, and Sinensis's website is dead/offline.
2025-03-27 00:02:31 +00:00
Chopper
7ad4d56426
DiskusScan: Fix popular and latestUpdate (#8016)
Fix loading popular and latestUpdate
2025-03-27 00:02:31 +00:00
Cuong-Tran
6205dad385
MangaDistrict: Preset genresList & preserve tagList order when saving (#8004) 2025-03-27 00:02:31 +00:00
Creepler13
fe96b31232
Include utils in sparse checkout tutorial (#7994)
include Utils in sparse checkout tutorial
2025-03-27 00:02:31 +00:00
Creepler13
21ee081352
TCBScans: Update version number (#7993)
update version
2025-03-27 00:02:31 +00:00
Creepler13
87e4096264
Tcbscans: Fix Search (#7956)
* Fix Search

* smol chane
2025-03-27 00:02:31 +00:00
Smol Ame
0e3bf78d8d
Mangaryu: Remove extension (#7987) 2025-03-27 00:02:31 +00:00
are-are-are
8482b7d235
VlogTruyen, LXHentai bump version (#7991)
* VlogTruyen bump version

* LxHentai bump version
2025-03-27 00:02:31 +00:00
Cuong-Tran
2ed6687b26
SushiScan: fix latest query (#7989)
* SushiScan: fix latest query

* bump version after merge

* missing page
2025-03-27 00:02:31 +00:00
Chopper
a20493a46c
YomuComics: Fix chapter list (#7982)
Fix chapter list
2025-03-27 00:02:28 +00:00
bapeey
08c73abe63
FMReader: Fallback to absolute date if relative date fails (#7980)
* use parseAbsoluteDate as fallback

* bump

* long
2025-03-27 00:02:28 +00:00
bapeey
0f1cbebf96
MadTheme: Fetch chapters like the site (#7978)
fetch chapters like the site
2025-03-27 00:02:28 +00:00
are-are-are
cd03d78928
Update some domain (#7972)
* SayHentai update domain

* HentaiVNPlus update domain

* VlogTruyen update domain & back to old name

* HentaiCB update domain
2025-03-27 00:02:28 +00:00
mrtear
ec62056e09
Update Domains (#7970)
* MilaSub

* TraduccionesAmistosas

* BlackoutComics

* TaurusFansub

* LxH: name + domain

* milasub f

* Taurus f

* again

* sorry guys

* LxH: id
2025-03-27 00:02:28 +00:00
kana-shii
5b8834fe6b
ZinChanManga(.net/.com): Fix URL (#7966)
* Update ZinChanManga.kt

* Update build.gradle

* Update ZinChanMangaCom.kt

* Update build.gradle

* Update src/en/zinchanmanga/build.gradle

Co-authored-by: mrtear <91820779+mrtear@users.noreply.github.com>

* Update src/en/zinchanmangacom/build.gradle

Co-authored-by: mrtear <91820779+mrtear@users.noreply.github.com>

* Update ZinChanManga.kt

---------

Co-authored-by: mrtear <91820779+mrtear@users.noreply.github.com>
2025-03-27 00:02:28 +00:00
MikeZeDev
88ffec340b
Holymanga : fixes chapters date parsing (#7958)
* HolyManga : fix chapetr date parsing

Closes https://github.com/keiyoushi/extensions-source/issues/7861

* Update HolyManga.kt

* Update HolyManga.kt

* Update build.gradle

* fix imports

* fix date parsing

* Update HolyManga.kt

* Update src/en/holymanga/src/eu/kanade/tachiyomi/extension/en/holymanga/HolyManga.kt

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:28 +00:00
kana-shii
03ee60f4e5
SeraphicDeviltry: Icon (#7957)
* add icon

* Update build.gradle

* high res icon

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

---------

Co-authored-by: Prem Kumar <60751338+prem-k-r@users.noreply.github.com>
2025-03-27 00:02:28 +00:00
mrtear
789e2ca044
Added MSYToon (#7971) 2025-03-27 00:02:28 +00:00
Vetle Ledaal
6098ea8e35
Set isNsfw explicitly for 348 extensions (#7859) 2025-03-27 00:02:28 +00:00
AwkwardPeak7
060473aedf
use Preferences functions from utils (#7757)
* import all temporarily

* use `getPreferencesLazy()`

regex: lazy\s*\{\s*Injekt\.get<Application>\(\)\.getSharedPreferences\(\"source_\$id\"\, 0x0000\)(!!)?\s*\}

* use `getPreferences()`

regex: Injekt\.get<Application>\(\)\.getSharedPreferences\(\"source_\$id\"\, 0x0000\)(!!)?

* `getPreferences` and `getPreferencesLazy` with migration

regex to find: getPreferences\(\)\s*\.

* preferences edge cases

* run lint

* why kotlinter not add newline

* bruh

* bump

* unused import not caught by kotlinter

* review suggestions on all files

* bump after merge main

* review

* reduce usage of
2025-03-27 00:02:28 +00:00
renovate[bot]
5aa7f1627c
Update dependency gradle to v8.13 (#7871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 00:02:28 +00:00
Creepler13
a44693f757
Add AuroraReset (#7947) 2025-03-27 00:02:28 +00:00
mrtear
023942f27e
Added ArtLapsa (#7880)
* ArtLapsa

* changes

- applied suggestions
- properly handles paid chapters in ArtLapsa

* f

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:28 +00:00
AlphaBoom
7b06cfa577
MangaGun: Fix image load (#7930) 2025-03-27 00:02:28 +00:00
Secozzi
d5acdde1d9
feat(en): Add NyxScans (#7883)
* feat(en): Add NyxScans

* naming

* Always use next data for page parsing

* Use functions from `utils`
2025-03-27 00:02:28 +00:00
Fioren
5e4c156a27
Remove self-promotion, ads: TopTruyen, DocTruyen3Q (#7882)
These sites added self-promotion and ad images that are not relevant to the translation team.
2025-03-27 00:02:28 +00:00
Cuong-Tran
4fc0bc0efd
Blogtruyen: site closed for months (#7857)
remove blogtruyen
2025-03-27 00:02:24 +00:00
Creepler13
70c74d2b55
PhilliaScans Fix Selectors (#7854)
Fix Selectors
2025-03-27 00:02:24 +00:00
bapeey
75dad8e026
Webtoons: Fix covers (#7848)
fix thumbnails
2025-03-27 00:02:24 +00:00
bapeey
cd28f381bf
LectorMoe(SenshiManga/Taikutsu): Filter out paid chapters and update api url (#7845)
* filter out chapters and update urls

* suscribersOnly
2025-03-27 00:02:24 +00:00
duongtra
7b52dd532e
TeamLanhLung: Update domain (#7839)
update domain
2025-03-27 00:02:24 +00:00
Prem Kumar
d0004a3440
Removed HikariScans (#7834)
removed hikari scans
2025-03-27 00:02:24 +00:00
dngonz
308b9a09d3
Inarimanga: Change domain (#7833)
change url
2025-03-27 00:02:24 +00:00
dngonz
fca1ceeb89
Catharsis World: Change url (#7813)
change url
2025-03-27 00:02:24 +00:00
dngonz
4defc529d3
Lua Scans: Fix date parse (#7803)
fix date parse
2025-03-27 00:02:24 +00:00
dngonz
5767234606
Add MangaBin (#7802) 2025-03-27 00:02:24 +00:00
AwkwardPeak7
4446491912
stonescape: fix description (#7800) 2025-03-27 00:02:24 +00:00
AwkwardPeak7
e2d5b6eb19
add RitharScans (#7799) 2025-03-27 00:02:24 +00:00
Hen
20f78a35bb
honeyManga: filter payed chapters (#7795)
* honeyManga: filter payed chapters

* honeyManga: checklist

* honeyManga: formatting
2025-03-27 00:02:24 +00:00
are-are-are
481fd54452
CbHentai: Add override baseUrl, Change baseUrl, Fix Regex (#7786)
* bump version

* Add override baseUrl, Change baseUrl, Fix Regex
2025-03-27 00:02:24 +00:00
Corvidae
25a118d501
build(SeraphicDeviltry): Add EN/ES source SeraphicDeviltry (#7734) (#7783)
* build(SeraphicDeviltry): Add EN/ES source SeraphicDeviltry (#7734)

* Update src/all/seraphicdeviltry/src/eu/kanade/tachiyomi/extension/all/seraphicdeviltry/SeraphicDeviltryFactory.kt

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

* build(SeraphicDeviltry): Remove open class, apply Factory proposed changes

---------

Co-authored-by: Kon <k@a.a>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-03-27 00:02:24 +00:00
Jake
ffd98958ee
Mangakakalot, Manganato: Updated Domain, Selectors and Filters (#7781)
* Mangakakalot - updated domain paths

* Mangakakalot - Fixed description query and filters

Also fixed certain cases that could result in 403 when opening chapters

* Mangakakalot - Updated baseUrl

* Manganato - Updated domain, selector, and filters

Manganato essentially became an exact copy mangakakalot so all changes (except for URLs) are the same with mangakakalot

* review changes, fixed upload date

* Replaced duplicated `GET` request logic with `super.imageRequest(page)` to avoid redundancy

* review changes, moved `SimpleDateFormat` outside the function
2025-03-27 00:02:24 +00:00
Creepler13
e8fed7ce6d
new Source Kanjiku (#7766)
* new Source Kanjiku

* changes

* lint

* lint again
2025-03-27 00:02:24 +00:00
Fioren
de846ee0ad
Add Domain Change: TopTruyen, DocTruyen3Q (#7765)
* Add Domain Change: TopTruyen, DocTruyen3Q

- Add Domain Change: TopTruyen, DocTruyen3Q
- Update Domain

* update

* remove unused import
2025-03-27 00:02:24 +00:00
dngonz
98b871591e
Knight No Scanlation: Fix selector (#7779)
fix selector
2025-03-27 00:02:24 +00:00
dngonz
7fbf5e8af6
Manga Crab: Fix url and selectors (#7778)
fix url and selectors
2025-03-27 00:02:24 +00:00
Corvidae
0239463aa8
fix(Siyahmelek): Domain Change (#7777)
Co-authored-by: Kon <k@a.a>
2025-03-27 00:02:24 +00:00
1500 changed files with 9470 additions and 8036 deletions

View File

@ -86,7 +86,7 @@ small, just do a normal full clone instead.**
```bash
git sparse-checkout set --cone --sparse-index
# add project folders
git sparse-checkout add buildSrc core gradle lib lib-multisrc
git sparse-checkout add buildSrc core gradle lib lib-multisrc utils
# add a single source
git sparse-checkout add src/<lang>/<source>
```

View File

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

Binary file not shown.

View File

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

2
gradlew generated vendored
View File

@ -205,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.bakkin
import android.app.Application
import android.os.Build
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
@ -33,9 +33,7 @@ abstract class BakkinReaderX(
"Android ${Build.VERSION.RELEASE}; Mobile) " +
"Tachiyomi/${AppInfo.getVersionName()}"
protected val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
protected val preferences by getPreferencesLazy()
private val json by lazy { Injekt.get<Json>() }

View File

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

View File

@ -19,18 +19,23 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.tryParse
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@ -46,9 +51,7 @@ abstract class ColaManga(
private val intl = ColaMangaIntl(lang)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences by getPreferencesLazy()
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(
@ -150,6 +153,8 @@ abstract class ColaManga(
protected abstract val genreTitle: String
protected abstract val statusOngoing: String
protected abstract val statusCompleted: String
protected abstract val lastUpdated: String
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1.fed-part-eone")!!.text()
@ -168,6 +173,15 @@ abstract class ColaManga(
override fun chapterListSelector(): String = "div:not(.fed-hidden) > div.all_data_list > ul.fed-part-rows a"
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }.apply {
if (isNotEmpty()) {
this[0].date_upload = dateFormat.tryParse(document.selectFirst("span.fed-text-muted:contains($lastUpdated) + a")?.text())
}
}
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.attr("title")

View File

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

View File

@ -28,7 +28,7 @@ open class ComicGamma(
private val json = Injekt.get<Json>()
override val client = network.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(SpeedBinbInterceptor(json))
.build()

View File

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

View File

@ -39,7 +39,7 @@ abstract class FansubsCat(
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
override val client: OkHttpClient = network.client
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()

View File

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

View File

@ -19,6 +19,7 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import java.nio.charset.Charset
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -246,7 +247,13 @@ abstract class FMReader(
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
}
}
date_upload = element.select(chapterTimeSelector).let { if (it.hasText()) parseRelativeDate(it.text()) else 0 }
date_upload = element.select(chapterTimeSelector).let { dateElement ->
if (dateElement.hasText()) {
parseRelativeDate(dateElement.text()).takeIf { it != 0L } ?: parseAbsoluteDate(dateElement.text())
} else {
0L
}
}
}
}
@ -257,55 +264,63 @@ abstract class FMReader(
open val dateWordIndex = 1
open fun parseRelativeDate(date: String): Long {
val value = date.split(' ')[dateValueIndex].toInt()
val dateWord = date.split(' ')[dateWordIndex].let {
if (it.contains("(")) {
it.substringBefore("(")
} else {
it.substringBefore("s")
try {
val value = date.split(' ')[dateValueIndex].toInt()
val dateWord = date.split(' ')[dateWordIndex].let {
if (it.contains("(")) {
it.substringBefore("(")
} else {
it.substringBefore("s")
}
}
}
// languages: en, vi, es, tr
return when (dateWord) {
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> {
return 0
// languages: en, vi, es, tr
return when (dateWord) {
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> {
return 0L
}
}
} catch (_: Exception) {
return 0L
}
}
open fun parseAbsoluteDate(dateStr: String): Long {
return runCatching { dateFormat.parse(dateStr)?.time }
.getOrNull() ?: 0L
return try {
dateFormat.parse(dateStr)?.time ?: 0L
} catch (_: ParseException) {
0L
}
}
open val pageListImageSelector = "img.chapter-img"

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.foolslide
import android.app.Application
import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
@ -12,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -273,9 +273,7 @@ abstract class FoolSlide(
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
protected val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
protected val preferences by getPreferencesLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
CheckBoxPreference(screen.context).apply {

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceScreen
@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -36,8 +36,6 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
@ -56,9 +54,7 @@ abstract class GalleryAdults(
.build()
/* Preferences */
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected val preferences: SharedPreferences by getPreferencesLazy()
protected open val useShortTitlePreference = true

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.grouple
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import eu.kanade.tachiyomi.network.GET
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@ -22,8 +22,6 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.DecimalFormat
import java.text.ParseException
@ -37,9 +35,7 @@ abstract class GroupLe(
final override val lang: String,
) : ConfigurableSource, ParsedHttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
override val supportsLatest = true

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.guya
import android.app.Application
import android.content.SharedPreferences
import android.os.Build
import androidx.preference.ListPreference
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
@ -25,8 +25,6 @@ import org.jsoup.Jsoup
import org.jsoup.select.Evaluator
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class Guya(
override val name: String,
@ -48,9 +46,7 @@ abstract class Guya(
private val scanlators: ScanlatorStore = ScanlatorStore()
// Preferences configuration
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
// Request builder for the "browse" page of the manga
override fun popularMangaRequest(page: Int): Request {

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.heancms
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -26,8 +26,6 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.concurrent.thread
@ -39,9 +37,7 @@ abstract class HeanCms(
protected val apiUrl: String = baseUrl.replace("://", "://api."),
) : ConfigurableSource, HttpSource() {
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected val preferences: SharedPreferences by getPreferencesLazy()
override val supportsLatest = true

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.hentaihand
import android.app.Application
import android.content.SharedPreferences
import android.text.InputType
import android.widget.Toast
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
@ -32,8 +32,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
@ -318,9 +316,7 @@ abstract class HentaiHand(
// Preferences
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))

View File

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

View File

@ -93,21 +93,24 @@ class Chapter(
private val id: Int,
private val slug: String,
private val number: JsonPrimitive,
private val createdBy: Name,
private val createdAt: String,
private val chapterStatus: String,
private val isAccessible: Boolean,
private val isLocked: Boolean? = false,
private val isTimeLocked: Boolean? = false,
private val mangaPost: ChapterPostDetails,
) {
fun isPublic() = chapterStatus == "PUBLIC"
fun isAccessible() = isAccessible
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
val prefix = if (isLocked()) "🔒 " else ""
val seriesSlug = mangaSlug ?: mangaPost.slug
url = "/series/$seriesSlug/$slug#$id"
name = "Chapter $number"
scanlator = createdBy.name
name = "${prefix}Chapter $number"
date_upload = try {
dateFormat.parse(createdAt)!!.time
} catch (_: ParseException) {

View File

@ -1,6 +1,10 @@
package eu.kanade.tachiyomi.multisrc.iken
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -9,32 +13,34 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class Iken(
override val name: String,
override val lang: String,
override val baseUrl: String,
) : HttpSource() {
val apiUrl: String = baseUrl,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.cloudflareClient
private val json by injectLazy<Json>()
private val preferences: SharedPreferences by getPreferencesLazy()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
private var genres = emptyList<Pair<String, String>>()
protected val titleCache by lazy {
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
val response = client.newCall(GET("$apiUrl/api/query?perPage=9999", headers)).execute()
val data = response.parseAs<SearchResponse>()
data.posts
@ -65,7 +71,7 @@ abstract class Iken(
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
val url = "$apiUrl/api/query".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
addQueryParameter("searchTerm", query.trim())
@ -114,35 +120,76 @@ abstract class Iken(
throw UnsupportedOperationException()
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url.substringAfterLast("#")
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
return GET(url, headers)
return GET("$baseUrl/series/${manga.url}", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<Post<ChapterListResponse>>()
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
val id = response.request.url.fragment!!
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
return data.post.chapters
.filter { it.isPublic() && it.isAccessible() }
.filter { it.isPublic() && (it.isAccessible() || (preferences.getBoolean(showLockedChapterPrefKey, false) && it.isLocked())) }
.map { it.toSChapter(data.post.slug) }
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("main section img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.absUrl("src"))
if (document.selectFirst("svg.lucide-lock") != null) {
throw Exception("Unlock chapter in webview")
}
return document.getNextJson("images").parseAs<List<PageParseDto>>().mapIndexed { idx, p ->
Page(idx, imageUrl = p.url)
}
}
@Serializable
class PageParseDto(
val url: String,
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = showLockedChapterPrefKey
title = "Show locked chapters"
setDefaultValue(false)
}.also(screen::addPreference)
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
protected fun Document.getNextJson(key: String): String {
val data = selectFirst("script:containsData($key)")
?.data()
?: throw Exception("Unable to retrieve NEXT data")
val keyIndex = data.indexOf(key)
val start = data.indexOf('[', keyIndex)
var depth = 1
var i = start + 1
while (i < data.length && depth > 0) {
when (data[i]) {
'[' -> depth++
']' -> depth--
}
i++
}
return "\"${data.substring(start, i)}\"".parseAs<String>()
}
}
private const val perPage = 18
private const val showLockedChapterPrefKey = "pref_show_locked_chapters"
private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""")

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.kemono
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -15,13 +14,12 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferences
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Thread.sleep
import java.util.TimeZone
@ -34,15 +32,14 @@ open class Kemono(
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.client.newBuilder().rateLimit(1).build()
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val preferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
private val preferences = getPreferences()
private val apiPath = "api/v1"

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.keyoapp
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -23,8 +23,6 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
@ -37,9 +35,7 @@ abstract class Keyoapp(
final override val lang: String,
) : ParsedHttpSource(), ConfigurableSource {
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected val preferences: SharedPreferences by getPreferencesLazy()
override val supportsLatest = true
@ -63,7 +59,8 @@ abstract class Keyoapp(
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularMangaSelector(): String = "div.flex-col div.grid > div.group.border"
override fun popularMangaSelector(): String =
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.getImageUrl("*[style*=background-image]")
@ -191,7 +188,7 @@ abstract class Keyoapp(
}
}
private fun genresRequest(): Request = GET("$baseUrl/series/", headers)
protected open fun genresRequest(): Request = GET("$baseUrl/series/", headers)
/**
* Get the genres from the search page document.

View File

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

View File

@ -111,7 +111,11 @@ abstract class LectorMoe(
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
val seriesSlug = result.data.slug
return result.data.chapters?.map { it.toSChapter(seriesSlug) } ?: emptyList()
return result.data.chapters
?.filter { it.subscribersOnly.not() }
?.map { it.toSChapter(seriesSlug) }
?.filter { it.date_upload < System.currentTimeMillis() }
?: emptyList()
}
override fun pageListRequest(chapter: SChapter): Request {

View File

@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
class Data<T>(val data: T)
@ -53,18 +54,21 @@ class SeriesAuthorDto(
val name: String,
)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@Serializable
class SeriesChapterDto(
private val title: String,
private val number: Float,
private val createdAt: String,
private val releasedAt: String,
val subscribersOnly: Boolean,
) {
fun toSChapter(seriesSlug: String) = SChapter.create().apply {
name = "Capítulo ${number.toString().removeSuffix(".0")} - $title"
date_upload = try {
dateFormat.parse(createdAt)?.time ?: 0L
dateFormat.parse(releasedAt)?.time ?: 0L
} catch (_: ParseException) {
0L
}

View File

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

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.multisrc.libgroup
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -57,9 +57,10 @@ abstract class LibGroup(
encodeDefaults = true
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrateOldImageServer()
private val preferences by getPreferencesLazy {
if (getString(SERVER_PREF, "main") == "fourth") {
edit().putString(SERVER_PREF, "secondary").apply()
}
}
override val supportsLatest = true
@ -685,11 +686,4 @@ abstract class LibGroup(
}
}
}
// api changed id of servers, remap SERVER_PREF old("fourth") to new("secondary")
private fun SharedPreferences.migrateOldImageServer(): SharedPreferences {
if (getString(SERVER_PREF, "main") != "fourth") return this
edit().putString(SERVER_PREF, "secondary").apply()
return this
}
}

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Application
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -30,8 +30,6 @@ import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
@ -51,9 +49,7 @@ abstract class MachineTranslations(
override val lang = language.lang
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected val preferences: SharedPreferences by getPreferencesLazy()
/**
* A flag that tracks whether the settings have been changed. It is used to indicate if

View File

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

View File

@ -160,7 +160,7 @@ abstract class Madara(
}
// exclude/filter bilibili manga from list
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector"
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
open val popularMangaUrlSelector = "div.post-title a"
@ -584,7 +584,7 @@ abstract class Madara(
return MangasPage(entries, hasNextPage)
}
override fun searchMangaSelector() = "div.c-tabs-item__content"
override fun searchMangaSelector() = "div.c-tabs-item__content , .manga__item"
protected open val searchMangaUrlSelector = "div.post-title a"
@ -754,7 +754,7 @@ abstract class Madara(
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
open val mangaDetailsSelectorArtist = "div.artist-content > a"
open val mangaDetailsSelectorStatus = "div.summary-content"
open val mangaDetailsSelectorStatus = "div.summary-content, div.summary-heading:contains(Status) + div"
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
open val mangaDetailsSelectorThumbnail = "div.summary_image img"
open val mangaDetailsSelectorGenre = "div.genres-content a"

View File

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

View File

@ -41,6 +41,10 @@ abstract class MadTheme(
.rateLimit(1, 1, TimeUnit.SECONDS)
.build()
protected open val useLegacyApi = false
protected open val useSlugSearch = false
// TODO: better cookie sharing
// TODO: don't count cached responses against rate limit
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
@ -177,59 +181,57 @@ abstract class MadTheme(
val document = response.asJsoup()
// Need the total chapters to check against the request
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
?.substringAfter("(")
?.substringBefore(")")
?.toIntOrNull()
val script = document.selectFirst("script:containsData(bookId)")
?: throw Exception("Cannot find script")
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
// Use slug search by default
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
if (!slugRequest.isSuccessful) {
throw Exception("HTTP error ${slugRequest.code}")
var chaptersList = document.select(chapterListSelector()).map { chapterFromElement(it) }
val fetchApi = document.selectFirst("div#show-more-chapters > span")
?.attr("onclick")?.equals("getChapters()")
?: false
if (fetchApi) {
val apiChapters = client.newCall(GET(buildChapterUrl(bookId, bookSlug), headers)).execute()
.asJsoup().select(chapterListSelector()).map { chapterFromElement(it) }
val cutIndex = chaptersList.indexOfFirst { chapter ->
apiChapters.any { it.url == chapter.url }
}.takeIf { it != -1 } ?: chaptersList.size
chaptersList = (chaptersList.subList(0, cutIndex) + apiChapters)
}
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
if (totalChapters != null && finalDocument.size < totalChapters) {
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
finalDocument = idRequest.asJsoup().select(chapterListSelector())
}
return finalDocument.map {
SChapter.create().apply {
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
name = it.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
}
}
return chaptersList
}
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
private fun buildChapterUrl(mangaId: String, mangaSlug: String): HttpUrl {
return baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addPathSegment("manga")
addPathSegment(fetchByParam)
addPathSegment(if (useSlugSearch) mangaSlug else mangaId)
addPathSegment("chapters")
addQueryParameter("source", "detail")
}.build()
}
override fun chapterListRequest(manga: SManga): Request =
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title)
.fragment("idFound")
.build()
override fun chapterListRequest(manga: SManga): Request {
if (useLegacyApi) {
val mangaId = MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)
val url = mangaId?.let {
"$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", it)
.addQueryParameter("manga_name", manga.title)
.fragment("idFound")
.build()
.toString()
} ?: (baseUrl + manga.url)
GET(url, headers)
} ?: GET("$baseUrl${manga.url}", headers)
return GET(url, headers)
}
return GET(baseUrl + manga.url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {

View File

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

View File

@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.multisrc.mangabox
import android.annotation.SuppressLint
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
@ -9,42 +13,144 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.TimeZone
import java.util.regex.Pattern
// Based off of Mangakakalot 1.2.8
abstract class MangaBox(
override val name: String,
override val baseUrl: String,
private val mirrorEntries: Array<String>,
override val lang: String,
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH),
) : ParsedHttpSource() {
private val dateFormat: SimpleDateFormat = SimpleDateFormat(
"MMM-dd-yyyy HH:mm",
Locale.ENGLISH,
).apply {
timeZone = TimeZone.getTimeZone("UTC")
},
) : ParsedHttpSource(), ConfigurableSource {
override val supportsLatest = true
override val baseUrl: String get() = mirror
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(::useAltCdnInterceptor)
.build()
private fun SharedPreferences.getMirrorPref(): String =
getString(PREF_USE_MIRROR, mirrorEntries[0])!!
private val preferences: SharedPreferences by getPreferencesLazy {
// if current mirror is not in mirrorEntries, set default
if (getMirrorPref() !in mirrorEntries.map { "${URL_PREFIX}$it" }) {
edit().putString(PREF_USE_MIRROR, "${URL_PREFIX}${mirrorEntries[0]}").apply()
}
}
private var mirror = ""
get() {
if (field.isNotEmpty()) {
return field
}
field = preferences.getMirrorPref()
return field
}
private val cdnSet =
MangaBoxLinkedCdnSet() // Stores all unique CDNs that the extension can use to retrieve chapter images
private class MangaBoxFallBackTag // Custom empty class tag to use as an identifier that the specific request is fallback-able
private fun HttpUrl.getBaseUrl(): String =
"${URL_PREFIX}${this.host}${
when (this.port) {
80, 443 -> ""
else -> ":${this.port}"
}
}"
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
val originalResponse: Response? = try {
chain.proceed(request)
} catch (e: IOException) {
if (requestTag == null) {
throw e
} else {
null
}
}
if (requestTag == null || originalResponse?.isSuccessful == true) {
requestTag?.let {
// Move working cdn to first so it gets priority during iteration
cdnSet.moveItemToFirst(request.url.getBaseUrl())
}
return originalResponse!!
}
// Close the original response if it's not successful
originalResponse?.close()
for (cdnUrl in cdnSet) {
var tryResponse: Response? = null
try {
val newUrl = cdnUrl.toHttpUrl().newBuilder()
.encodedPath(request.url.encodedPath)
.fragment(request.url.fragment)
.build()
// Create a new request with the updated URL
val newRequest = request.newBuilder()
.url(newUrl)
.build()
// Proceed with the new request
tryResponse = chain.proceed(newRequest)
// Check if the response is successful
if (tryResponse.isSuccessful) {
// Move working cdn to first so it gets priority during iteration
cdnSet.moveItemToFirst(newRequest.url.getBaseUrl())
return tryResponse
}
tryResponse.close()
} catch (_: IOException) {
tryResponse?.close()
}
}
// If all CDNs fail, throw an error
return throw IOException("All CDN attempts failed.")
}
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", baseUrl) // for covers
.add("Referer", "$baseUrl/")
open val popularUrlPath = "manga_list?type=topview&category=all&state=all&page="
open val popularUrlPath = "manga-list/hot-manga?page="
open val latestUrlPath = "manga_list?type=latest&category=all&state=all&page="
open val latestUrlPath = "manga-list/latest-manga?page="
open val simpleQueryPath = "search/"
open val simpleQueryPath = "search/story/"
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
@ -58,10 +164,11 @@ abstract class MangaBox(
return GET("$baseUrl/$latestUrlPath$page", headers)
}
protected fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
private fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
return SManga.create().apply {
element.select(urlSelector).first()!!.let {
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
url = it.attr("abs:href")
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
title = it.text()
}
thumbnail_url = element.select("img").first()!!.attr("abs:src")
@ -72,62 +179,47 @@ abstract class MangaBox(
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
override fun popularMangaNextPageSelector() = "div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
override fun popularMangaNextPageSelector() =
"div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
return if (query.isNotBlank()) {
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
.addPathSegment(normalizeSearchQuery(query))
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
} else {
val url = baseUrl.toHttpUrl().newBuilder()
if (getAdvancedGenreFilters().isNotEmpty()) {
url.addPathSegment("advanced_search")
url.addQueryParameter("page", page.toString())
url.addQueryParameter("keyw", normalizeSearchQuery(query))
var genreInclude = ""
var genreExclude = ""
filters.forEach { filter ->
when (filter) {
is KeywordFilter -> filter.toUriPart()?.let { url.addQueryParameter("keyt", it) }
is SortFilter -> url.addQueryParameter("orby", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("sts", filter.toUriPart())
is AdvGenreFilter -> {
filter.state.forEach { if (it.isIncluded()) genreInclude += "_${it.id}" }
filter.state.forEach { if (it.isExcluded()) genreExclude += "_${it.id}" }
}
else -> {}
}
}
url.addQueryParameter("g_i", genreInclude)
url.addQueryParameter("g_e", genreExclude)
} else {
url.addPathSegment("manga_list")
url.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
else -> {}
}
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
url.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
else -> {}
}
}
GET(url.build(), headers)
}
}
override fun searchMangaSelector() = ".panel_story_list .story_item"
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
override fun searchMangaNextPageSelector() = "a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
override fun searchMangaNextPageSelector() =
"a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
open val descriptionSelector = "div#noidungm, div#panel-story-info-description"
open val descriptionSelector = "div#noidungm, div#panel-story-info-description, div#contentBox"
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
@ -146,11 +238,15 @@ abstract class MangaBox(
return SManga.create().apply {
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
title = infoElement.select("h1, h2").first()!!.text()
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a").eachText().joinToString()
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a")
.eachText().joinToString()
status = parseStatus(
infoElement.select("li:contains(status), td:containsOwn(status) + td").text(),
)
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
?.select("a")?.joinToString { it.text() } // kakalot
?: infoElement.select("td:containsOwn(genres) + td a").joinToString { it.text() } // nelo
?: infoElement.select("td:containsOwn(genres) + td a")
.joinToString { it.text() } // nelo
} ?: checkForRedirectMessage(document)
description = document.select(descriptionSelector).firstOrNull()?.ownText()
?.replace("""^$title summary:\s""".toRegex(), "")
@ -201,42 +297,21 @@ abstract class MangaBox(
private fun Element.selectDateFromElement(): Element {
val defaultChapterDateSelector = "span"
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(alternateChapterDateSelector).last()!!
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(
alternateChapterDateSelector,
).last()!!
}
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a").let {
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
url = it.attr("abs:href")
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
name = it.text()
scanlator =
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
}
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
}
}
private fun parseChapterDate(date: String, host: String): Long? {
return if ("ago" in date) {
val value = date.split(' ')[0].toIntOrNull()
val cal = Calendar.getInstance()
when {
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) }
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) }
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -value) }
else -> null
}?.timeInMillis
} else {
try {
if (host.contains("manganato", ignoreCase = true)) {
// Nelo's date format
SimpleDateFormat("MMM dd,yy", Locale.ENGLISH).parse(date)
} else {
dateformat.parse(date)
}
} catch (e: ParseException) {
null
}?.time
date_upload = dateFormat.tryParse(element.selectDateFromElement().attr("title"))
}
}
@ -247,26 +322,59 @@ abstract class MangaBox(
return super.pageListRequest(chapter)
}
open val pageListSelector = "div#vungdoc img, div.container-chapter-reader img"
private fun extractArray(scriptContent: String, arrayName: String): List<String> {
val pattern = Pattern.compile("$arrayName\\s*=\\s*\\[([^]]+)]")
val matcher = pattern.matcher(scriptContent)
val arrayValues = mutableListOf<String>()
if (matcher.find()) {
val arrayContent = matcher.group(1)
val values = arrayContent?.split(",")
if (values != null) {
for (value in values) {
arrayValues.add(
value.trim()
.removeSurrounding("\"")
.replace("\\/", "/")
.removeSuffix("/"),
)
}
}
}
return arrayValues
}
override fun pageListParse(document: Document): List<Page> {
return document.select(pageListSelector)
// filter out bad elements for mangakakalots
.filterNot { it.attr("src").endsWith("log") }
.mapIndexed { i, element ->
val url = element.attr("abs:src").let { src ->
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
"https://images.weserv.nl/?url=" + src.substringAfter("//")
} else {
src
}
}
Page(i, document.location(), url)
val element = document.select("head > script").lastOrNull()
?: return emptyList()
val cdns =
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
val chapterImages = extractArray(element.html(), "chapterImages")
// Add all parsed cdns to set
cdnSet.addAll(cdns)
return chapterImages.mapIndexed { i, imagePath ->
val parsedUrl = cdns[0].toHttpUrl().run {
newBuilder()
.encodedPath(
"/$imagePath".replace(
"//",
"/",
),
) // replace ensures that there's at least one trailing slash prefix
.build()
.toString()
}
Page(i, document.location(), parsedUrl)
}
}
override fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
return GET(page.imageUrl!!, headers).newBuilder()
.tag(MangaBoxFallBackTag::class.java, MangaBoxFallBackTag()).build()
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -282,46 +390,27 @@ abstract class MangaBox(
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
str = str.replace("đ".toRegex(), "d")
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
str = str.replace(
"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(),
"_",
)
str = str.replace("_+_".toRegex(), "_")
str = str.replace("""^_+|_+$""".toRegex(), "")
return str
}
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
FilterList(
KeywordFilter(getKeywordFilters()),
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
AdvGenreFilter(getAdvancedGenreFilters()),
)
} else {
FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
GenreFilter(getGenreFilters()),
)
}
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
GenreFilter(getGenreFilters()),
)
private class KeywordFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Keyword search ", vals)
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
// For advanced search, specifically tri-state genres
private class AdvGenreFilter(vals: List<AdvGenre>) : Filter.Group<AdvGenre>("Category", vals)
class AdvGenre(val id: String?, name: String) : Filter.TriState(name)
// keyt query parameter
private fun getKeywordFilters(): Array<Pair<String?, String>> = arrayOf(
Pair(null, "Everything"),
Pair("title", "Title"),
Pair("alternative", "Alt title"),
Pair("author", "Author"),
)
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("latest", "Latest"),
Pair("newest", "Newest"),
@ -337,53 +426,72 @@ abstract class MangaBox(
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("all", "ALL"),
Pair("2", "Action"),
Pair("3", "Adult"),
Pair("4", "Adventure"),
Pair("6", "Comedy"),
Pair("7", "Cooking"),
Pair("9", "Doujinshi"),
Pair("10", "Drama"),
Pair("11", "Ecchi"),
Pair("12", "Fantasy"),
Pair("13", "Gender bender"),
Pair("14", "Harem"),
Pair("15", "Historical"),
Pair("16", "Horror"),
Pair("45", "Isekai"),
Pair("17", "Josei"),
Pair("44", "Manhua"),
Pair("43", "Manhwa"),
Pair("19", "Martial arts"),
Pair("20", "Mature"),
Pair("21", "Mecha"),
Pair("22", "Medical"),
Pair("24", "Mystery"),
Pair("25", "One shot"),
Pair("26", "Psychological"),
Pair("27", "Romance"),
Pair("28", "School life"),
Pair("29", "Sci fi"),
Pair("30", "Seinen"),
Pair("31", "Shoujo"),
Pair("32", "Shoujo ai"),
Pair("33", "Shounen"),
Pair("34", "Shounen ai"),
Pair("35", "Slice of life"),
Pair("36", "Smut"),
Pair("37", "Sports"),
Pair("38", "Supernatural"),
Pair("39", "Tragedy"),
Pair("40", "Webtoons"),
Pair("41", "Yaoi"),
Pair("42", "Yuri"),
Pair("action", "Action"),
Pair("adult", "Adult"),
Pair("adventure", "Adventure"),
Pair("comedy", "Comedy"),
Pair("cooking", "Cooking"),
Pair("doujinshi", "Doujinshi"),
Pair("drama", "Drama"),
Pair("ecchi", "Ecchi"),
Pair("fantasy", "Fantasy"),
Pair("gender-bender", "Gender bender"),
Pair("harem", "Harem"),
Pair("historical", "Historical"),
Pair("horror", "Horror"),
Pair("isekai", "Isekai"),
Pair("josei", "Josei"),
Pair("manhua", "Manhua"),
Pair("manhwa", "Manhwa"),
Pair("martial-arts", "Martial arts"),
Pair("mature", "Mature"),
Pair("mecha", "Mecha"),
Pair("medical", "Medical"),
Pair("mystery", "Mystery"),
Pair("one-shot", "One shot"),
Pair("psychological", "Psychological"),
Pair("romance", "Romance"),
Pair("school-life", "School life"),
Pair("sci-fi", "Sci fi"),
Pair("seinen", "Seinen"),
Pair("shoujo", "Shoujo"),
Pair("shoujo-ai", "Shoujo ai"),
Pair("shounen", "Shounen"),
Pair("shounen-ai", "Shounen ai"),
Pair("slice-of-life", "Slice of life"),
Pair("smut", "Smut"),
Pair("sports", "Sports"),
Pair("supernatural", "Supernatural"),
Pair("tragedy", "Tragedy"),
Pair("webtoons", "Webtoons"),
Pair("yaoi", "Yaoi"),
Pair("yuri", "Yuri"),
)
// To be overridden if using tri-state genres
protected open fun getAdvancedGenreFilters(): List<AdvGenre> = emptyList()
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_USE_MIRROR
title = "Mirror"
entries = mirrorEntries
entryValues = mirrorEntries.map { "${URL_PREFIX}$it" }.toTypedArray()
setDefaultValue(entryValues[0])
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
// Update values
mirror = newValue as String
true
}
}.let(screen::addPreference)
}
companion object {
private const val PREF_USE_MIRROR = "pref_use_mirror"
private const val URL_PREFIX = "https://"
}
}

View File

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.multisrc.mangabox
class MangaBoxLinkedCdnSet : LinkedHashSet<String>() {
fun moveItemToFirst(item: String) {
// Lock the object to avoid multi threading issues
synchronized(this) {
if (this.contains(item) && this.first() != item) {
// Remove the item from the current set
this.remove(item)
// Create a new list with the item at the first position
val newItems = mutableListOf(item)
// Add the remaining items
newItems.addAll(this)
// Clear the current set and add all items from the new list
this.clear()
this.addAll(newItems)
}
}
}
}

View File

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

View File

@ -48,7 +48,7 @@ abstract class MangaEsp(
protected open val useApiSearch = false
override val client: OkHttpClient = network.client.newBuilder()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()

View File

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

View File

@ -17,7 +17,6 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -31,7 +30,7 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
@ -49,6 +48,7 @@ abstract class MangaHub(
private var baseApiUrl = "https://api.mghcdn.com"
private var baseCdnUrl = "https://imgx.mghcdn.com"
private val regex = Regex("mhub_access=([^;]+)")
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
@ -91,8 +91,6 @@ abstract class MangaHub(
}
private fun refreshApiKey(chapter: SChapter) {
val now = Calendar.getInstance().time.time
val slug = "$baseUrl${chapter.url}"
.toHttpUrlOrNull()
?.pathSegments
@ -104,32 +102,28 @@ abstract class MangaHub(
baseUrl.toHttpUrl()
}
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
// Set required cookie (for cache busting?)
val recently = buildJsonObject {
putJsonObject((now - (0..3600).random()).toString()) {
put("mangaID", (1..42_000).random())
put("number", (1..20).random())
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
try {
val response = client.newCall(GET("$url$query", headers)).execute()
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
} catch (_: IOException) {
throw IOException("An error occurred while obtaining a new API key") // Show error
}
}.toString()
client.cookieJar.saveFromResponse(
url,
listOf(
Cookie.Builder()
.domain(url.host)
.name("recently")
.value(URLEncoder.encode(recently, "utf-8"))
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
.build(),
),
)
val request = GET("$url?reloadKey=1", headers)
client.newCall(request).execute()
}
}
data class SMangaDTO(

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -16,8 +16,6 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.ref.SoftReference
import java.text.SimpleDateFormat
import java.util.Locale
@ -34,14 +32,12 @@ abstract class MangaThemesiaAlt(
protected open val listUrl = "$mangaUrlDirectory/list-mode/"
protected open val listSelector = "div#content div.soralist ul li a.series"
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).also {
if (it.contains("__random_part_cache")) {
it.edit().remove("__random_part_cache").apply()
}
if (it.contains("titles_without_random_part")) {
it.edit().remove("titles_without_random_part").apply()
}
protected val preferences by getPreferencesLazy {
if (contains("__random_part_cache")) {
edit().remove("__random_part_cache").apply()
}
if (contains("titles_without_random_part")) {
edit().remove("titles_without_random_part").apply()
}
}

View File

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

View File

@ -33,7 +33,7 @@ open class MCCMS(
private val json: Json by injectLazy()
override val client by lazy {
network.client.newBuilder()
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}

View File

@ -26,7 +26,7 @@ open class MCCMSWeb(
override val supportsLatest get() = true
override val client by lazy {
network.client.newBuilder()
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}

View File

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

View File

@ -26,7 +26,7 @@ abstract class MultiChan(
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(2)

View File

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

View File

@ -40,7 +40,7 @@ abstract class Senkuro(
.add("Content-Type", "application/json")
override val client: OkHttpClient =
network.client.newBuilder()
network.cloudflareClient.newBuilder()
.rateLimit(3)
.build()

View File

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

View File

@ -34,7 +34,7 @@ abstract class SinMH(
protected open val mobileUrl = _baseUrl.replaceFirst("www.", "m.")
override val supportsLatest = true
override val client = network.client.newBuilder().rateLimit(2).build()
override val client = network.cloudflareClient.newBuilder().rateLimit(2).build()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)

View File

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

View File

@ -45,7 +45,7 @@ open class Webtoons(
override val supportsLatest = true
override val client: OkHttpClient = super.client.newBuilder()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
@ -197,7 +197,7 @@ open class Webtoons(
open fun parseDetailsThumbnail(document: Document): String? {
val picElement = document.select("#content > div.cont_box > div.detail_body")
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")")
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")").removeSurrounding("\"").removeSurrounding("'")
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
}

View File

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

View File

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

View File

@ -0,0 +1,206 @@
package eu.kanade.tachiyomi.multisrc.yuyu
import android.net.Uri
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.net.URLEncoder
abstract class YuYu(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val client = network.cloudflareClient
override val supportsLatest = true
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = ".top10-section .top10-item a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.absUrl("href"))
}
override fun popularMangaNextPageSelector() = null
// ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.build()
return GET(url, headers)
}
override fun latestUpdatesSelector() = ".manga-list .manga-card"
override fun latestUpdatesNextPageSelector() = "a.page-link:contains(>)"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
val url = element.selectFirst("a.manga-cover")!!.absUrl("href")
val uri = Uri.parse(url)
val pathSegments = uri.pathSegments
val lastSegment = URLEncoder.encode(pathSegments.last(), "UTF-8")
val encodedUrl = uri.buildUpon()
.path(pathSegments.dropLast(1).joinToString("/") + "/$lastSegment")
.toString()
title = element.selectFirst("a.manga-title")!!.text()
thumbnail_url = element.selectFirst("a.manga-cover img")?.absUrl("data-src")
setUrlWithoutDomain(encodedUrl)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
return MangasPage(mangas, document.hasNextPage())
}
private fun Document.hasNextPage() =
selectFirst(latestUpdatesNextPageSelector())?.absUrl("href")?.let {
selectFirst("a.page-link.active")
?.absUrl("href")
.equals(it, ignoreCase = true).not()
} ?: false
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("search", query)
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map {
val manga = mangaDetailsParse(it.asJsoup())
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-result-item"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(".search-result-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(
element.attr("onclick").let {
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
},
)
}
override fun searchMangaNextPageSelector() = null
// ============================== Manga Details =========================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val details = document.selectFirst(".manga-banner .container")!!
title = details.selectFirst("h1")!!.text()
thumbnail_url = details.selectFirst("img")?.absUrl("src")
genre = details.select(".genre-tag").joinToString { it.text() }
description = details.selectFirst(".sinopse p")?.text()
details.selectFirst(".manga-meta > div")?.ownText()?.let {
status = when (it.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
private fun SManga.fetchMangaId(): String {
val document = client.newCall(mangaDetailsRequest(this)).execute().asJsoup()
return document.select("script")
.map(Element::data)
.firstOrNull(MANGA_ID_REGEX::containsMatchIn)
?.let { MANGA_ID_REGEX.find(it)?.groups?.get(1)?.value }
?: throw Exception("Manga ID não encontrado")
}
// ============================== Chapters ===============================
override fun chapterListSelector() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".capitulo-numero")!!.ownText()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
return Observable.just(chapters)
}
private fun fetchChapterListPage(mangaId: String, page: Int): Response {
val url = "$baseUrl/ajax/lzmvke.php?order=DESC".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("page", page.toString())
.build()
return client
.newCall(GET(url, headers))
.execute()
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select("picture img").mapIndexed { idx, element ->
Page(idx, imageUrl = element.absUrl("src"))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================== Utilities ===========================
@Serializable
class ChaptersDto(val chapters: String, private val remaining: Int) {
fun hasNext() = remaining > 0
}
companion object {
const val PREFIX_SEARCH = "id:"
val SEARCH_URL_REGEX = "'([^']+)".toRegex()
val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.yushukemangas
package eu.kanade.tachiyomi.multisrc.yuyu
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class YushukeMangasUrlActivity : Activity() {
class YuYuUrlActivity : Activity() {
private val tag = javaClass.simpleName
@ -17,7 +17,7 @@ class YushukeMangasUrlActivity : Activity() {
if (pathSegment != null && pathSegment.size > 1) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
putExtra("query", "${YuYu.PREFIX_SEARCH}${pathSegment[1]}")
putExtra("filter", packageName)
}

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.akuma
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
@ -25,8 +25,6 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
@ -48,12 +46,12 @@ class Akuma(
private var storedToken: String? = null
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
private val ddosGuardIntercept = DDosGuardInterceptor(network.cloudflareClient)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override val client: OkHttpClient = network.client.newBuilder()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(ddosGuardIntercept)
.addInterceptor(::tokenInterceptor)
.rateLimit(2)
@ -112,9 +110,7 @@ class Akuma(
return storedToken!!
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
private val displayFullTitle: Boolean get() = preferences.getBoolean(PREF_TITLE, false)

View File

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

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@ -48,10 +49,7 @@ open class BatoTo(
private val siteLang: String,
) : ConfigurableSource, ParsedHttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrateMirrorPref()
}
private val preferences by getPreferencesLazy { migrateMirrorPref() }
override val name: String = "Bato.to"
override val baseUrl: String get() = mirror
@ -125,14 +123,12 @@ open class BatoTo(
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
}
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
private fun SharedPreferences.migrateMirrorPref() {
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

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comic Fury'
extClass = '.ComicFuryFactory'
extVersionCode = 3
extVersionCode = 5
isNsfw = true
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.comicfury
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -16,13 +15,12 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
@ -35,7 +33,7 @@ class ComicFury(
override val name: String = "Comic Fury$extraName" // Used for No Text
override val supportsLatest: Boolean = true
override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build()
override val client = network.cloudflareClient.newBuilder().addInterceptor(TextInterceptor()).build()
/**
* Archive is on a separate page from manga info
@ -205,9 +203,7 @@ class ComicFury(
private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
// START OF AUTHOR NOTES //
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
companion object {
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
}

View File

@ -4,6 +4,7 @@ ext {
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

@ -1,5 +1,8 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description
include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres
@ -9,6 +12,9 @@ group_tags_off=List all tags together
update_cover_title=Update Covers
update_cover_on=Keep cover updated
update_cover_off=Prefer first cover
local_title_title=Translated Title
local_title_on=if available
local_title_off=Use the default title from the site
score_position_title=Score Position in the Description
score_position_top=Top
score_position_middle=Middle

View File

@ -1,5 +1,8 @@
ignored_groups_title=Grupos Ignorados
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
show_alternative_titles_title=Mostrar Títulos Alternativos
show_alternative_titles_on=Adiciona títulos alternativos à descrição
show_alternative_titles_off=Não mostra títulos alternativos na descrição
include_tags_title=Incluir Tags
include_tags_on=Mais detalhadas, mas podem conter spoilers
include_tags_off=Apenas os gêneros básicos
@ -9,6 +12,9 @@ group_tags_off=Listar todas as tags juntas
update_cover_title=Atualizar Capas
update_cover_on=Manter capas atualizadas
update_cover_off=Usar apenas a primeira capa
local_title_title=Título Traduzido
local_title_on=se disponível
local_title_off=Usar o título padrão do site
score_position_title=Posição da Nota na Descrição
score_position_top=Topo
score_position_middle=Meio

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -17,16 +16,16 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Builder
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -65,10 +64,7 @@ abstract class Comick(
)
}
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.newLineIgnoredGroups()
}
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
@ -83,6 +79,20 @@ abstract class Comick(
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"]
summaryOn = intl["show_alternative_titles_on"]
summaryOff = intl["show_alternative_titles_off"]
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = INCLUDE_MU_TAGS_PREF
title = intl["include_tags_title"]
@ -125,6 +135,20 @@ abstract class Comick(
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = LOCAL_TITLE_PREF
title = intl["local_title_title"]
summaryOff = intl["local_title_off"]
summaryOn = intl["local_title_on"]
setDefaultValue(LOCAL_TITLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SCORE_POSITION_PREF
title = intl["score_position_title"]
@ -160,6 +184,9 @@ abstract class Comick(
.orEmpty()
.toSet()
private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
@ -169,6 +196,17 @@ abstract class Comick(
private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
private val SharedPreferences.localTitle: String
get() = if (getBoolean(
LOCAL_TITLE_PREF,
LOCAL_TITLE_DEFAULT,
)
) {
comickLang.lowercase()
} else {
"all"
}
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
@ -277,6 +315,16 @@ abstract class Comick(
return MangasPage(entries, end < searchResponse.size)
}
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").forEach {
builder.addQueryParameter(
parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
filters.forEach { it ->
@ -298,7 +346,7 @@ abstract class Comick(
}
is DemographicFilter -> {
it.state.filter { it.isIncluded() }.forEach {
it.state.filter { it.state }.forEach {
addQueryParameter("demographic", it.value)
}
}
@ -319,6 +367,12 @@ abstract class Comick(
}
}
is ContentRatingFilter -> {
if (it.state > 0) {
addQueryParameter("content_rating", it.getValue())
}
}
is CreatedAtFilter -> {
if (it.state > 0) {
addQueryParameter("time", it.getValue())
@ -345,13 +399,13 @@ abstract class Comick(
is TagFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").forEach {
addQueryParameter(
"tags",
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
addTagQueryParameters(this, it.state, "tags")
}
}
is ExcludedTagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "excluded-tags")
}
}
@ -405,14 +459,18 @@ abstract class Comick(
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
@ -507,8 +565,9 @@ abstract class Comick(
override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups(): SharedPreferences {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return this
private fun SharedPreferences.newLineIgnoredGroups() {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit()
@ -522,14 +581,14 @@ abstract class Comick(
)
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply()
return this
}
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
const val INCLUDE_MU_TAGS_DEFAULT = false
private const val GROUP_TAGS_PREF = "GroupTags"
@ -539,6 +598,8 @@ abstract class Comick(
private const val FIRST_COVER_DEFAULT = true
private const val SCORE_POSITION_PREF = "ScorePosition"
const val SCORE_POSITION_DEFAULT = "top"
private const val LOCAL_TITLE_PREF = "LocalTitle"
private const val LOCAL_TITLE_DEFAULT = false
private const val LIMIT = 20
private const val CHAPTERS_LIMIT = 99999
}

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
@ -36,13 +37,20 @@ class Manga(
fun toSManga(
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
scorePosition: String = SCORE_POSITION_DEFAULT,
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
covers: List<MDcovers>? = null,
groupTags: Boolean = GROUP_TAGS_DEFAULT,
) =
SManga.create().apply {
titleLang: String,
): SManga {
val entryTitle = comic.altTitles.firstOrNull {
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
}?.title ?: comic.title
val titles = listOf(Title(title = comic.title)) + comic.altTitles
return SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = comic.title
title = entryTitle
description = buildString {
if (scorePosition == "top") append(comic.fancyScore)
val desc = comic.desc?.beautifyDescription()
@ -54,13 +62,14 @@ class Manga(
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
if (comic.altTitles.isNotEmpty()) {
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append("Alternative Titles:\n")
append(
comic.altTitles.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
titles.distinctBy { it.title }.filter { it.title != entryTitle }
.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
)
}
if (scorePosition == "bottom") {
@ -95,6 +104,7 @@ class Manga(
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
}
}
}
@Serializable
@ -169,6 +179,7 @@ class MDcovers(
@Serializable
class Title(
val title: String?,
val lang: String? = null,
)
@Serializable

View File

@ -11,6 +11,7 @@ fun getFilters(): FilterList {
TypeFilter("Type", getTypeList),
SortFilter("Sort", getSortsList),
StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"),
CreatedAtFilter("Created at", getCreatedAtList),
MinimumFilter("Minimum Chapters"),
@ -20,6 +21,7 @@ fun getFilters(): FilterList {
ToYearFilter("To"),
Filter.Header("Separate tags with commas"),
TagFilter("Tags"),
ExcludedTagFilter("Excluded Tags"),
)
}
@ -29,8 +31,10 @@ internal class GenreFilter(name: String, genreList: List<Pair<String, String>>)
internal class TagFilter(name: String) : TextFilter(name)
internal class ExcludedTagFilter(name: String) : TextFilter(name)
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
Filter.Group<TriFilter>(name, demographicList.map { TriFilter(it.first, it.second) })
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
@ -52,6 +56,9 @@ internal class SortFilter(name: String, sortList: List<Pair<String, String>>, st
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
/** Generics **/
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
@ -156,12 +163,14 @@ private val getDemographicList: List<Pair<String, String>> = listOf(
Pair("Shoujo", "2"),
Pair("Seinen", "3"),
Pair("Josei", "4"),
Pair("None", "5"),
)
private val getTypeList: List<Pair<String, String>> = listOf(
Pair("Manga", "jp"),
Pair("Manhwa", "kr"),
Pair("Manhua", "cn"),
Pair("Others", "others"),
)
private val getCreatedAtList: List<Pair<String, String>> = listOf(
@ -190,3 +199,10 @@ private val getStatusList: List<Pair<String, String>> = listOf(
Pair("Cancelled", "3"),
Pair("Hiatus", "4"),
)
private val getContentRatingList: List<Pair<String, String>> = listOf(
Pair("All", ""),
Pair("Safe", "safe"),
Pair("Suggestive", "suggestive"),
Pair("Erotica", "erotica"),
)

View File

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

View File

@ -62,7 +62,7 @@ open class Comico(
this["Origin"] = baseUrl
}.build()
override val client = network.client.newBuilder()
override val client = network.cloudflareClient.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.comicskingdom
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
@ -12,6 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
@ -19,8 +19,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
@ -301,9 +299,7 @@ class ComicsKingdom(override val lang: String) : ConfigurableSource, HttpSource(
screen.addPreference(compactpref)
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
private fun shouldCompact() = preferences.getBoolean("compactPref", true)

View File

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

View File

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
@ -75,9 +76,7 @@ open class Comikey(
classLoader = this::class.java.classLoader!!,
)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences by getPreferencesLazy()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers)

View File

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

View File

@ -1,51 +1,46 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Danbooru : ParsedHttpSource() {
class Danbooru : HttpSource(), ConfigurableSource {
override val name: String = "Danbooru"
override val baseUrl: String = "https://danbooru.donmai.us"
override val lang: String = "all"
override val supportsLatest: Boolean = true
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
override val client = network.cloudflareClient
private val dateFormat: SimpleDateFormat by lazy {
private val dateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
}
private val preference by getPreferencesLazy()
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList())
override fun popularMangaFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun popularMangaNextPageSelector(): String =
searchMangaNextPageSelector()
override fun popularMangaSelector(): String =
searchMangaSelector()
override fun popularMangaParse(response: Response) =
searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
@ -87,86 +82,100 @@ class Danbooru : ParsedHttpSource() {
return GET(url.build(), headers)
}
override fun searchMangaSelector(): String =
"article.post-preview"
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(".post-preview-link")?.attr("href")!!
title = element.selectFirst("div.text-center")?.text() ?: ""
val entries = document.select("article.post-preview").map {
searchMangaFromElement(it)
}
val hasNextPage = document.selectFirst("a.paginator-next") != null
return MangasPage(entries, hasNextPage)
}
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(".post-preview-link")!!.attr("href")
title = element.selectFirst("div.text-center")!!.text()
thumbnail_url = element.selectFirst("source")?.attr("srcset")
?.substringAfterLast(',')?.trim()
?.substringBeforeLast(' ')?.trimStart()
}
override fun searchMangaNextPageSelector(): String =
"a.paginator-next"
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(FilterOrder("created_at")))
override fun latestUpdatesSelector(): String =
searchMangaSelector()
override fun latestUpdatesParse(response: Response): MangasPage =
searchMangaParse(response)
override fun latestUpdatesFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
override fun latestUpdatesNextPageSelector(): String =
searchMangaNextPageSelector()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst(".pool-category-series, .pool-category-collection")?.text() ?: ""
description = document.getElementById("description")?.wholeText() ?: ""
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
title = document.selectFirst(".pool-category-series, .pool-category-collection")!!.text()
description = document.getElementById("description")?.wholeText()
author = document.selectFirst("#description a[href*=artists]")?.ownText()
artist = author
update_strategy = if (!preference.splitChaptersPref) {
UpdateStrategy.ONLY_FETCH_ONCE
} else {
UpdateStrategy.ALWAYS_UPDATE
}
}
override fun chapterListRequest(manga: SManga): Request =
GET("$baseUrl${manga.url}.json?only=id,created_at", headers)
GET("$baseUrl${manga.url}.json", headers)
override fun chapterListParse(response: Response): List<SChapter> = listOf(
SChapter.create().apply {
val data = json.decodeFromString<JsonObject>(response.body.string())
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<Pool>()
val id = data["id"]!!.jsonPrimitive.content
val createdAt = data["created_at"]?.jsonPrimitive?.content
url = "/pools/$id"
name = "Oneshot"
date_upload = createdAt?.let(::parseTimestamp) ?: 0
chapter_number = 0F
},
)
override fun chapterListSelector(): String =
throw IllegalStateException("Not used")
override fun chapterFromElement(element: Element): SChapter =
throw IllegalStateException("Not used")
return if (preference.splitChaptersPref) {
data.postIds.mapIndexed { index, id ->
SChapter.create().apply {
url = "/posts/$id"
name = "Post ${index + 1}"
chapter_number = index + 1f
}
}.reversed().apply {
if (isNotEmpty()) {
this[0].date_upload = dateFormat.tryParse(data.updatedAt)
}
}
} else {
listOf(
SChapter.create().apply {
url = "/pools/${data.id}"
name = "Oneshot"
date_upload = dateFormat.tryParse(data.updatedAt)
chapter_number = 0F
},
)
}
}
override fun pageListRequest(chapter: SChapter): Request =
GET("$baseUrl${chapter.url}.json?only=post_ids", headers)
GET("$baseUrl${chapter.url}.json", headers)
override fun pageListParse(response: Response): List<Page> =
json.decodeFromString<JsonObject>(response.body.string())
.get("post_ids")?.jsonArray
?.map { it.jsonPrimitive.content }
?.mapIndexed { i, id -> Page(index = i, url = "/posts/$id") }
?: emptyList()
if (response.request.url.toString().contains("/posts/")) {
val data = response.parseAs<Post>()
override fun pageListParse(document: Document): List<Page> =
throw IllegalStateException("Not used")
listOf(
Page(index = 0, imageUrl = data.fileUrl),
)
} else {
val data = response.parseAs<Pool>()
data.postIds.mapIndexed { index, id ->
Page(index, url = "/posts/$id")
}
}
override fun imageUrlRequest(page: Page): Request =
GET("$baseUrl${page.url}.json?only=file_url", headers)
GET("$baseUrl${page.url}.json", headers)
override fun imageUrlParse(response: Response): String =
json.decodeFromString<JsonObject>(response.body.string())
.get("file_url")!!.jsonPrimitive.content
override fun imageUrlParse(document: Document): String =
throw IllegalStateException("Not used")
response.parseAs<Post>().fileUrl
override fun getChapterUrl(chapter: SChapter): String =
baseUrl + chapter.url
@ -181,6 +190,20 @@ class Danbooru : ParsedHttpSource() {
),
)
private fun parseTimestamp(string: String): Long? =
runCatching { dateFormat.parse(string)?.time!! }.getOrNull()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_LIST_PREF
title = "Split posts into individual chapters"
summary = """
Instead of showing one 'OneShot' chapter,
each post will be it's own chapter
""".trimIndent()
setDefaultValue(false)
}.also(screen::addPreference)
}
private val SharedPreferences.splitChaptersPref: Boolean
get() = getBoolean(CHAPTER_LIST_PREF, false)
}
private const val CHAPTER_LIST_PREF = "prefChapterList"

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Pool(
val id: Int,
@SerialName("updated_at") val updatedAt: String,
@SerialName("post_ids") val postIds: List<Int>,
)
@Serializable
class Post(
@SerialName("file_url") val fileUrl: String,
)

View File

@ -1,4 +1,5 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import eu.kanade.tachiyomi.source.model.Filter
internal class FilterTags : Filter.Text("Tags")

View File

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

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.deviantart
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
@ -13,6 +12,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
@ -20,8 +20,6 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -32,9 +30,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override val lang = "all"
override val supportsLatest = false
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
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")

View File

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

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.webkit.CookieManager
@ -23,6 +22,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
@ -30,8 +30,6 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URLEncoder
abstract class EHentai(
@ -41,9 +39,7 @@ abstract class EHentai(
override val name = "E-Hentai"
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
private val memberId: String by lazy { getMemberIdPref() }

View File

@ -1,7 +1,7 @@
ext {
extName = 'Galaxy'
extClass = '.GalaxyFactory'
extVersionCode = 4
extVersionCode = 5
isNsfw = false
}

View File

@ -1,13 +1,11 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.SourceFactory
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import keiyoushi.utils.getPreferencesLazy
class GalaxyFactory : SourceFactory {
@ -22,9 +20,7 @@ class GalaxyFactory : SourceFactory {
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val preferences: SharedPreferences by getPreferencesLazy()
companion object {
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"

View File

@ -4,6 +4,7 @@ ext {
themePkg = 'madara'
baseUrl = 'https://grabber.zone'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

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

View File

@ -1,13 +1,8 @@
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
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -15,34 +10,28 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
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
import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.LinkedList
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
import kotlin.time.Duration.Companion.seconds
@ -50,35 +39,25 @@ import kotlin.time.Duration.Companion.seconds
class Hitomi(
override val lang: String,
private val nozomiLang: String,
) : HttpSource(), ConfigurableSource {
) : HttpSource() {
override val name = "Hitomi"
private val domain = "hitomi.la"
private val cdnDomain = "gold-usergeneratedcontent.net"
override val baseUrl = "https://$domain"
override val baseUrl = "https://hitomi.la"
private val ltnUrl = "https://ltn.$domain"
private val ltnUrl = "https://ltn.$cdnDomain"
override val supportsLatest = true
private val json: Json by injectLazy()
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::jxlContentTypeInterceptor)
.addInterceptor(::updateImageUrlInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!!
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
@ -523,7 +502,7 @@ class Hitomi(
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
"https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
}
description = buildString {
japaneseTitle?.let {
@ -568,11 +547,7 @@ class Hitomi(
name = "Chapter"
url = gallery.galleryurl
scanlator = gallery.type
date_upload = try {
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
} catch (_: ParseException) {
0L
}
date_upload = dateFormat.tryParse(gallery.date.substringBeforeLast("-"))
},
)
}
@ -589,28 +564,18 @@ class Hitomi(
return GET("$ltnUrl/galleries/$id.js", headers)
}
override fun pageListParse(response: Response) = runBlocking {
override fun pageListParse(response: Response): List<Page> {
val gallery = response.parseScriptAs<Gallery>()
val id = gallery.galleryurl
.substringAfterLast("-")
.substringBefore(".")
gallery.files.mapIndexed { idx, img ->
val hash = img.hash
val typePref = imageType()
val avif = img.hasavif == 1 && typePref == "avif"
val jxl = img.hasjxl == 1 && typePref == "jxl"
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
val imageUrl = when {
jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl"
avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif"
else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp"
}
return gallery.files.mapIndexed { idx, img ->
// actual logic in updateImageUrlInterceptor
val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
.fragment(img.hash)
.build()
.toString()
Page(
idx,
@ -636,7 +601,7 @@ class Hitomi(
val body = use { it.body.string() }
val transformed = transform(body)
return json.decodeFromString(transformed)
return transformed.parseAs()
}
private suspend fun Call.awaitSuccess() =
@ -698,45 +663,6 @@ class Hitomi(
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_IMAGETYPE
title = "Images Type"
entries = arrayOf("webp", "avif", "jxl")
entryValues = arrayOf("webp", "avif", "jxl")
summary = "Clear chapter cache to apply changes"
setDefaultValue("webp")
}.also(screen::addPreference)
}
private fun List<Int>.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray()
private val signatureOne = listOf(0xFF, 0x0A).toBytesList()
private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList()
fun ByteArray.startsWith(byteArray: ByteArray): Boolean {
if (this.size < byteArray.size) return false
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
}
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headers["Content-Type"] != "application/octet-stream") {
return response
}
val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong()
val bytesArray = response.peekBody(bytesPeek).bytes()
if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) {
return response
}
val type = "image/jxl"
val body = response.body.bytes().toResponseBody(type.toMediaType())
return response.newBuilder()
.body(body)
.header("Content-Type", type)
.build()
}
private fun streamResetRetry(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
@ -753,21 +679,22 @@ class Hitomi(
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
val (ext, hash) = match.destructured
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
if (request.url.host != "127.0.0.1") {
return chain.proceed(request)
}
return chain.proceed(request)
val hash = request.url.fragment!!
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
val newRequest = request.newBuilder()
.url(imageUrl)
.build()
return chain.proceed(newRequest)
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
@ -777,8 +704,4 @@ class Hitomi(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
const val PREF_IMAGETYPE = "pref_image_type"
}
}

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