160 Commits

Author SHA1 Message Date
4a8c7fa36a Fix urlDecode usage 2024-02-07 09:33:33 +02:00
1251a6cd75 Merge pull request #548 from Ah3ron/patch-1
Fix tag filtering for Russian characters
2024-02-07 09:11:57 +02:00
Ah3ron
0ce8eae969 Fix tag filtering for Russian characters 2024-02-05 19:45:35 +03:00
devi
8b9c76fca5 LerYaoi #512
StellarSaber #533
Update Url ( #530 )
Remove dead sources
2024-02-03 11:00:02 +01:00
52e4e6b802 Update collections library 2024-01-31 15:04:00 +02:00
57c9d26916 Fix android compatibility 2024-01-31 14:45:39 +02:00
19d7003a9e [BatoTo] Fix pages parsing #521 #522 2024-01-31 14:31:06 +02:00
b11e50ab22 Fixes batch 2024-01-30 13:09:27 +02:00
a03ab0e0c0 Update tests 2024-01-30 11:16:28 +02:00
05d4db00b3 [ResetScans] Fix chapters names #518 2024-01-30 11:05:41 +02:00
f4aaa14fc5 Simplify workflow 2024-01-30 10:36:25 +02:00
a8f9423307 Migrate chapter number to float 2024-01-22 13:45:36 +02:00
7c871edbca Merge pull request #503 from mangadventure/master
Add MangAdventure sources (Arc-Relight, Assorted Scans)
2024-01-22 13:14:35 +02:00
ObserverOfTime
f7d9579cee Relax pagination test 2024-01-20 19:27:16 +02:00
ObserverOfTime
b7e6ca8a26 Add MangAdventure sources
- Arc-Relight
- Assorted Scans
2024-01-20 19:27:16 +02:00
8852d1e22e [Remanga] Attempt to fix No value for page id 2024-01-19 23:31:21 +03:00
fcaa0ea442 Add volume field to chapter 2024-01-17 15:03:33 +02:00
8e7d7e0bde Add some sources 2024-01-15 01:58:21 +03:00
scaledzdn
50c6c9f842 src: id/shinigami.kt: Change url source (#481)
* Previous url got banned on Indonesia
2024-01-13 13:40:06 +03:00
a2979753a9 [Grouple] Fix pages parsing 2024-01-12 18:10:58 +02:00
789e39b6cb Fixes batch 2024-01-11 17:36:07 +02:00
2f5441ef20 [Remanga] Use home url as authUrl 2024-01-09 20:15:56 +03:00
devi
6ad08f13d4 Add Comic1000, ThunderScans, WhaleManga, SussyScan, EDoujin, 3XYaoi, SussyScan #444
Url TeamXNovel, WestManga
Remove AzureManga, BabelToon, FranxxMangas, LsHistoria, MangaGenki, Wowomik, MiraiScans, Nonbiri, PeaceScans, ReaperScansTr, TurkToon
2024-01-09 17:51:16 +01:00
devi
656f621711 Remove bad import 2024-01-07 14:51:15 +01:00
devi
6dc0e12fb0 add filter on sources
Remove Komikgo redirect to ManhwaLand
Add Template PizzaReaderParser and move FmTeam
Add PhoenixScans, TuttoAnimeManga, LupiTeam, LadyEstelarScan, GtoTheGreatSite
2024-01-07 14:50:16 +01:00
0e8579017b [BatoTo] Fix pages parsing 2024-01-07 12:02:13 +02:00
devi
e03d0efe71 Fix FlixScansOrg and add filter 2024-01-05 19:24:36 +01:00
0b497c4e0b [ComicK] Fix branches names 2024-01-05 16:22:32 +02:00
devi
704f589b6d Add new filter on sources
Url GekkouScans, SssScanlator
Add LunarScan #435
2024-01-04 19:10:22 +01:00
3feb84ac9e Update some domains 2024-01-04 13:12:14 +02:00
e375654009 [ComickFun] Fix branches and titles 2024-01-04 13:06:37 +02:00
ce7ac5d8f4 [ExHentai] Fix tags and branch 2024-01-04 13:02:56 +02:00
devi
a172fa5a5b Add new filter on sources
Add BurningScans
2024-01-03 19:33:45 +01:00
7c89f53988 [ExHentai] Improve tags loading 2024-01-03 11:54:56 +02:00
4e2d739840 [Shinigami] Add user-agent changing option 2024-01-02 20:34:37 +03:00
devi
5cdbda700b Urls : DomalFansub, HentaiVN, PheTruyen, TruyentranhLH
Add : KomikSay, MangaLesen
Remove : 1stManhwa, ConsejoDeMatones, OzulScans, SugarLab, HyoManga, Mi2Manga
2024-01-02 17:53:45 +01:00
b274b51699 Improve filtering 2024-01-02 14:17:20 +02:00
efb5d34279 Merge pull request #431 from KotatsuApp/ExcludeTags 2024-01-02 12:51:43 +02:00
devi
9ecb75b51c Add ZScanlation, KomikGesn , Kenhuav2Scan
Fix MangaMate
2023-12-31 11:41:45 +01:00
69332a85da Add isTagsExclusionSupported flag 2023-12-31 09:51:09 +02:00
ea095084cc Fix some parsers 2023-12-31 09:27:17 +02:00
1f176ebab2 Update dependencies 2023-12-31 08:32:20 +02:00
devi
327c05d03e Fix Chapters madtheme
Url NettruyenBing
Remove Dead Sources :
AkuManga , CmReader, FalconManga, JaiminisBox, Manga3s, MangaCv, MangaLeks, UniToonOficial , 1stKissManga.tv, Manhwa2read, ManhuaMix, ManhuaDex, Hwago, ImmortalUpdates, KomikSay, MangaTx, Scan-Fr.org, QueenScans, PhantomScans, OpScanlations, NeuManga.net, ManhwaFreak.fr, Lady Estelar Scan, KomikManhwa, KomikManga, KaratcamScans, Boosei, LyraScans, MangaDiyari, MangaPro, NHentai.uk, Oxapk, SpiderScans, MangaNelo.biz, MangaOnline.team, SpartanManga
2023-12-30 11:09:56 +01:00
devi
2d7c120d19 Add Feature Content Rating Filter 2023-12-29 14:59:50 +01:00
devi
a2e9f19814 Fix Build 2023-12-29 14:16:56 +01:00
devi
c602355e65 Feature Add Exclude Tags 2023-12-29 13:57:01 +01:00
devi
a390e0de49 Fix ALPHABETICAL Sort on BentomangaParser
Remove Toonily.net
Add MonzeeKomik, Tresdaos
2023-12-29 13:55:04 +01:00
a228d71d57 Merge pull request #429 from KotatsuApp/Alpha-zaAndStateUpcoming
Add a new filter and a new state
2023-12-28 16:43:35 +02:00
devi
b2f0eaccc4 Remove Import Local 2023-12-28 10:55:20 +01:00
devi
07aaec7d3c Add template Scan and move ScanVfOrg in
Add ScanIta

Remove Mangastic -> redirect to toon69
Remove MangaGoYaoi -> redirect to asurascans.us
Remove Cizgiromanarsivi -> redirect to mangaoku.info
Remove WebtoonEvreni -> redirect to merlinscans.com
Remove GalaxyAction -> redirect to flixscans.com
Remove GalaxyAction -> redirect to kingofshojo.com
Remove aquamanga.live -> redirect to kingofshojo.com
Remove MangaReaderPro -> redirect to hadesnofansub.com

Urls : S2Manga.io, KomikzoId, Toon69, ReaperComics, ShadowXManga, TeamXNovel, YaoiFlix, MangaEmpress, MangaMukai.com, komikRemaja.cfd, MangaKyo, KomikMirror, ManhwaDesu, ManhwaIndo.sbs, ManhwaLand, Wowomik, LireScanVf.com, NhattruyenPlus
2023-12-28 10:54:52 +01:00
devi
392e9af944 New SortOrder.ALPHABETICAL_DESC
New MangaState.UPCOMING
2023-12-26 15:17:40 +01:00
904e0719eb [HitomiLa] Fix tag names 2023-12-25 19:48:33 +02:00
52ac3d2e6c Merge pull request #428 from AwkwardPeak7/hitomi
HitomiLa: fix thumbnails
2023-12-25 19:21:54 +02:00
devi
859b07e454 Add Ler999, ShadowCeviri 2023-12-25 17:02:06 +01:00
AwkwardPeak7
d4b252fd71 HitomiLa: fix thumbnails
I dismissed this in their scripts as the thumbnails worked fine but now they broke
2023-12-25 19:28:41 +05:00
4a0e7221b0 [HitomiLa] Refactor 2023-12-25 13:08:19 +02:00
43bdbe5a01 Merge pull request #426 from AwkwardPeak7/hitomi
Hitomi.la
2023-12-25 12:51:17 +02:00
3d10456a87 Apply suggestions from code review 2023-12-25 12:50:56 +02:00
d5a6c95ffb [MangaInUa] Fix chapters loading 2023-12-25 12:40:31 +02:00
devi
edc07b5dbd New Template GuyaParser
Add DankeFursLesen, GuyaCubari, Hachirumi, MahouShoujobu, Olaoe
Add local on sources GattsuParser
2023-12-24 12:00:06 +01:00
devi
a5219ceb6c New Template GattsuParser
Add Baozimh, MuitoHentai, TopReadManhwa, HentaiSeason, HentaiTokyo, MundoHentaiOficial, UniversoHentai
2023-12-23 18:59:48 +01:00
AwkwardPeak7
039075086e hitomi: better locale in tag search 2023-12-23 22:57:12 +05:00
AwkwardPeak7
495c9fad33 hitomi: formatting 2023-12-23 22:23:43 +05:00
AwkwardPeak7
b61c5e8f12 hitomi: locales 2023-12-23 20:29:16 +05:00
AwkwardPeak7
a40a8d329a simplify search a bit 2023-12-23 19:55:21 +05:00
AwkwardPeak7
3f3ab5a7be Hitomi.la 2023-12-23 19:14:51 +05:00
devi
d8c32047d0 Fix MangaATrend
Add LerManga
Remove ReaperScansPt.kt
2023-12-22 17:23:52 +01:00
devi
f5f2854f36 Add OnePieceEx 2023-12-20 20:02:31 +01:00
devi
86a2b95141 Add Comicaso, LectorManga, Mangaoku, MangaSehri.net, MangaTr, TempestFansub.net
Fix KomikCast
Url ElarcPage , InfraFandub
2023-12-19 20:42:49 +01:00
4db7ccc853 Tiny refactoring 2023-12-19 08:11:19 +02:00
b1da5c2924 [Mangaowl] Fix chapters parsing 2023-12-19 07:45:56 +02:00
devi
0fe6fed838 Merge pull request #418 from PhantomShift/patch-1
Fix MangaDex nsfw check
2023-12-17 18:09:23 +01:00
PhantomShift
a3f8c0a236 Fix MangaDex nsfw check
Additionally check for pornographic content rating when labeling a work as NSFW.
2023-12-17 10:49:12 -05:00
devi
c88d7de138 Add AComics, ReaperComics ( en ) 2023-12-17 14:48:54 +01:00
devi
eeafb3c6fd Fix JiangzaiToon #417 2023-12-17 11:58:29 +01:00
devi
0208f5be97 Add DatgarScanlation, HyoManga, HeckScans, MerlinScans, TraduccionesMoonlight, NinjaScan, Mi2MangaEs, ScanVfOrg
Url InariManga
Fix chapters MangaGeko
2023-12-16 17:24:54 +01:00
42ecf8a958 Merge pull request #413 from KotatsuApp/ZeistMangaParser
Add ZeistMangaParser
2023-12-16 08:06:03 +02:00
d780623b98 Merge pull request #406 from AwkwardPeak7/mplus
MangaPlus
2023-12-16 07:49:17 +02:00
devi
1b1a167580 Add EpikMan 2023-12-15 23:06:29 +01:00
devi
49d0c80070 Add MaxgsScan, WolfScanBr 2023-12-15 22:51:27 +01:00
devi
a35c2f3394 Add ZeistMangaParser and sources
add urldecode
ul VfScan
2023-12-15 22:04:47 +01:00
AwkwardPeak7
3034a87929 MangaPlus: divide different languages to avoid too many calls during chapter fetching & prevent some non-english titles to be filtered out 2023-12-15 18:34:27 +05:00
AwkwardPeak7
33b96043c9 Merge remote-tracking branch 'upstream/master' into mplus 2023-12-15 18:07:24 +05:00
devi
4d02dc0d12 Move FlixScansOrg to En
add MangasOnline , WorldManhwas
Url Shinigami
2023-12-14 18:18:18 +01:00
AwkwardPeak7
79fd2a58c1 MangaPlus: improvements 2023-12-14 19:19:32 +05:00
AwkwardPeak7
ab82d0eba0 MangaPlus 2023-12-13 21:18:57 +05:00
75cc0716fd Merge pull request #404 from AwkwardPeak7/999 2023-12-13 17:59:59 +02:00
87f99addbb Update SuspendLazy 2023-12-13 12:59:26 +02:00
AwkwardPeak7
3e1b15a23c review changes 2023-12-12 23:35:07 +05:00
devi
5758e9f68f Add BentoScan, Manga18.xyz, NewManhua
Url MangaScan, Anime-Sama.me
Fix DoujinDesu.tv
2023-12-12 18:29:01 +01:00
AwkwardPeak7
0964c02a44 largeCover and better chapter branch name 2023-12-12 18:55:15 +05:00
AwkwardPeak7
bf06ab7492 use SuspendLazy for fetching cdn host 2023-12-12 17:27:38 +05:00
AwkwardPeak7
e992566899 999Hentai 2023-12-12 14:01:36 +05:00
devi
97338f31c5 Add IrisScanlator #403
Add RagnaScan
Fix VexManga
2023-12-11 17:21:44 +01:00
devi
0ff4f976e1 Fix FlixScans
Add FlixScansOrg
Add DoujinKu
2023-12-10 20:28:35 +01:00
devi
496b3388ee Add source : MangaPark , GekkouScans, YuriLab 2023-12-09 17:19:43 +01:00
devi
d6b578acb0 Add, fix sources
Manual addition of lang on Bato.To
Add multi tag , lang and rework tags on exhentai
Add lang on imhentai
2023-12-08 17:18:23 +01:00
b3be87f541 Update readme 2023-12-08 07:27:35 +02:00
devi
786cd12dba Add, fix sources
Add language on Bato.To
2023-12-06 19:24:15 +01:00
devi
a1a17c02d7 Add, fix sources
Add language on Bato.To
2023-12-06 19:23:46 +01:00
devi
b8caf8e572 Minor update. 2023-12-05 17:37:07 +01:00
44c2c074a8 [MangaDex] Language filter support 2023-12-05 13:39:07 +02:00
3ed960254d [GalleryAdults] Language filter support #357 2023-12-05 13:05:11 +02:00
f1e8fbec5d Merge pull request #401 from KotatsuApp/filter
Rework getList
2023-12-05 11:26:17 +02:00
c5ed9cf54b Merge pull request #400 from deividgabrielpeira/master
Neoxscan change url
2023-12-05 11:25:03 +02:00
devi
65c567dbcb Fix NHentaiParser 2023-12-04 18:16:56 +01:00
devi
b312823afd Merge remote-tracking branch 'origin/master' 2023-12-03 12:57:14 +01:00
devi
56eb059bcd fix getlist VmpParser, LineWebtoonsParser, BlogTruyenParser, HentaiVNParser, YurinekoParser
add filter on WpComicsParser, ZMangaParser, LxManga, Truyenqq, TruyentranhLHParser

Move NetTruyen to template WpComicsParser and add multitags on WpComicsParser
2023-12-03 12:56:52 +01:00
devi
6ad78f1cba fix getlist OtakuSanctuaryParser, bakai , brmangas, LerMangaOnline, MangaOnline, YugenMangas, MangaAy, SadScans, YaoiFlix
add filter on SinmhParser, TrWebtoon
2023-12-02 16:41:37 +01:00
devi
26b758ae50 fix getlist some source mangaeader, MmrcmsParser
add filter on NepnepParser
2023-12-01 18:50:34 +01:00
devi
019b8cc087 fix getlist some source mangaeader, MmrcmsParser
add filter on NepnepParser
2023-12-01 18:49:30 +01:00
Deivid Gabriel Pereira de Oliveira
c95845083a Neoxscan change url 2023-12-01 16:48:41 +00:00
9c7f314f65 [HentaiUkr] Add usage of objects69.json 2023-12-01 02:48:00 +03:00
devi
743a92a3e8 remove import 2023-11-30 18:46:52 +01:00
devi
37e9b33c24 add filter : LikeMangaParser, FmTeam, MadthemeParser , MangaReaderParser
Fix GetList : NicovideoSeigaParser, Manga18Parser

add filter & multitags on MangaboxParser
2023-11-30 18:46:10 +01:00
devi
7c2ac033d7 add filter on HeanCms , DoujinDesu.tv
Fix GetList GalleryAdultsParser
2023-11-29 18:31:26 +01:00
0efd5437f9 [HentaiUkr] Fix objects loading 2023-11-29 17:18:01 +02:00
devi
db770351f3 add filter on FmreaderParser, LugnicaScans
Fix GetList on FoolSlideParser , FuryoSociety , LegacyScans, LireScan, ScansMangas.me, ScantradUnion
2023-11-28 21:07:54 +01:00
devi
14b2457627 fix getlist TempleScanEsp
fix getlist and add tag on Details mangas TuMangaOnline
2023-11-27 18:57:47 +01:00
devi
f62ea43de8 fix getlist on AnimeBootstrapParser, ImHentai, beetoon, CloneManga, MangaGeko, Po2Scans, Pururin
add filter FlixScans, TeamXNovel, ComicExtra, MangaOwl.to, MangaTown, Manhwa18.net, ManhwasMen

fix getlist and add tag list on DynastyScans
fix getlist and add SortOrder.UPDATED on MangaStorm
2023-11-26 18:44:34 +01:00
devi
ab2aad6bd0 add filter on bentomanga
add filter on ninemanga
2023-11-25 20:52:02 +01:00
devi
1293233ad9 fix getListPage Some sources madara 2023-11-25 20:04:47 +01:00
devi
5fff2adbfb MadaraParser add filter.states
- Some sources madara, need to be changed too
2023-11-25 17:57:38 +01:00
devi
7d67b718a5 Mangadex add filter.states and info states 2023-11-25 17:45:31 +01:00
devi
b8ca21d588 unwanted change 2023-11-25 17:37:04 +01:00
devi
44d91e43ad Add altName on ComicK
Change oneOrThrowIfMany on Bato.To
2023-11-25 17:34:52 +01:00
3bb10ee948 [HentaiUkr] Url hotfix 2023-11-25 19:22:42 +03:00
devi
81caa59a36 Add status filter support
Add oneOrThrowIfMany() on MangaState
2023-11-25 17:06:21 +01:00
46e863ef79 [ComicK] Fix manga state 2023-11-25 17:12:44 +02:00
cecba7623e [ComicK] Fixes and status filter support 2023-11-25 14:44:04 +02:00
7dee865c8a Merge pull request #397 from deividgabrielpeira/master
Daily Update (extensions portuguese)
2023-11-25 13:06:57 +02:00
Deivid Gabriel Pereira de Oliveira
5ac3cdb03c Arkham Scan -> HuntersScan (Fusion) 2023-11-24 12:28:54 +00:00
c3613f3ba4 [Grouple] Migrate to the new getList 2023-11-23 12:20:30 +02:00
e7ac1c1feb Fix PagedMangaParsers 2023-11-23 12:13:32 +02:00
24573d12f5 Fix compiling 2023-11-23 11:32:14 +02:00
9d2f633e14 Merge branch 'feature/filter-ex' 2023-11-23 11:20:26 +02:00
570b0c19c5 Merge pull request #388 from deividgabrielpeira/master
Daily Update (extensions portuguese)
2023-11-23 11:15:34 +02:00
Deivid Gabriel Pereira de Oliveira
a11e42a4d6 Merge branch 'KotatsuApp:master' into master 2023-11-19 14:50:31 -03:00
devi
410c73a74f Add multi tag some sources
Add SortOrder.POPULARITY some sources
Add all language on GalleryAdultsParser
fix YugenMangas (pt)
2023-11-19 17:58:10 +01:00
devi
3ea088371b Minor daily update 2023-11-18 20:44:35 +01:00
Deivid Gabriel Pereira de Oliveira
09a0bb364a HentaiSSSSSS (is closed) 2023-11-18 17:30:45 +00:00
Deivid Gabriel Pereira de Oliveira
d91947ae15 NocturneSummer fix? 2023-11-18 17:21:44 +00:00
Deivid Gabriel Pereira de Oliveira
0a8a9e5935 NocturneSummer fix 2023-11-18 17:18:31 +00:00
Deivid Gabriel Pereira de Oliveira
59226b298f apenasmaisumyaoi fix 2023-11-18 17:16:24 +00:00
Deivid Gabriel Pereira de Oliveira
dc41bf3e83 ArthurScan Fix 2023-11-18 16:06:30 +00:00
Deivid Gabriel Pereira de Oliveira
a790a18f7f test 2023-11-18 16:04:21 +00:00
Deivid Gabriel Pereira de Oliveira
1fcc1096f5 ArthurScan add Catalog (pt) and (test) 2023-11-18 15:59:47 +00:00
Deivid Gabriel Pereira de Oliveira
86aaca271f Merge branch 'KotatsuApp:master' into master 2023-11-18 12:41:55 -03:00
Deivid Gabriel Pereira de Oliveira
b8cc2d6953 Animaregia (Down) 2023-11-18 14:48:56 +00:00
Deivid Gabriel Pereira de Oliveira
31459ea6e7 yaoitoshokan (closed the site) 2023-11-18 12:58:48 +00:00
Deivid Gabriel Pereira de Oliveira
7ab84688c2 LerYaoi (down), posting in telegram 2023-11-18 12:56:12 +00:00
Deivid Gabriel Pereira de Oliveira
d18f287804 NocturneSummer fix 2023-11-18 12:44:41 +00:00
Deivid Gabriel Pereira de Oliveira
1c3b41acba Nocsummer, yaoi site, necessary (ContentType.HENTAI) 2023-11-18 12:42:15 +00:00
Deivid Gabriel Pereira de Oliveira
ecfc7e40c9 HiperCool, change url 2023-11-18 12:29:55 +00:00
41eea1c420 Set jvmTarget to 8 2023-11-18 14:28:13 +02:00
Deivid Gabriel Pereira de Oliveira
594524afc3 apenasmaisumyaoi change name, possibly the url will be changed 2023-11-18 12:16:12 +00:00
Deivid Gabriel Pereira de Oliveira
70a5337604 AkumanoTenshi -> tatakaeScant (fusion) 2023-11-18 12:12:02 +00:00
Deivid Gabriel Pereira de Oliveira
924fde8790 CeriseScans, change url 2023-11-18 11:58:45 +00:00
dea70974bf Revert unwanted changes 2023-11-18 13:03:09 +02:00
43fd7b8d47 [HoneyManga] Fix images urls 2023-11-18 12:55:02 +02:00
492 changed files with 12858 additions and 6646 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
pull_request:
paths:
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/site/**'
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
permissions:
contents: read
@@ -19,9 +19,4 @@ jobs:
java-version: '11'
distribution: 'temurin'
cache: 'gradle'
- run: ./gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest" || true
- run: ./gradlew generateTestsReport
- uses: actions/upload-artifact@v3
with:
name: Report
path: build/test-results-html/TEST-org.koitharu.kotatsu.parsers.MangaParserTest.htm
- run: ./gradlew assemble

3
.gitignore vendored
View File

@@ -29,6 +29,7 @@
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/ktlint-plugin.xml
.idea/*.iml
.idea/modules
*.iml
@@ -76,4 +77,4 @@ build/
.idea/codeStyles/
src/test/resources/cookies.txt
local.properties
local.properties

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="1.9.22" />
</component>
</project>

View File

@@ -37,6 +37,12 @@ This library provides manga sources.
Versions are available on [JitPack](https://jitpack.io/#KotatsuApp/kotatsu-parsers)
When used in Android
projects, [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) with
the [NIO specification](https://developer.android.com/studio/write/java11-nio-support-table) should be enabled to
support Java 8+ features.
3. Usage in code
```kotlin
@@ -45,8 +51,8 @@ This library provides manga sources.
`mangaLoaderContext` is an implementation of the `MangaLoaderContext` class.
See examples
of [Android](https://github.com/KotatsuApp/Kotatsu/blob/devel/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt)
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/main/kotlin/org/koitharu/kotatsu_dl/env/MangaLoaderContextImpl.kt)
of [Android](https://github.com/KotatsuApp/Kotatsu/blob/devel/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt)
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/jvmMain/kotlin/org/koitharu/kotatsu_dl/logic/MangaLoaderContextImpl.kt)
implementation.
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.

View File

@@ -2,8 +2,8 @@ import tasks.ReportGenerateTask
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '1.9.20'
id 'com.google.devtools.ksp' version '1.9.20-1.0.14'
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
id 'maven-publish'
}
@@ -36,7 +36,7 @@ compileTestKotlin {
}
kotlin {
jvmToolchain(11)
jvmToolchain(8)
sourceSets {
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
}
@@ -55,16 +55,16 @@ afterEvaluate {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okio:okio:3.6.0'
api 'org.jsoup:jsoup:1.16.2'
implementation 'com.squareup.okio:okio:3.7.0'
api 'org.jsoup:jsoup:1.17.2'
implementation 'org.json:json:20231013'
implementation 'androidx.collection:collection-ktx:1.3.0'
implementation 'androidx.collection:collection:1.4.0'
ksp project(':kotatsu-parsers-ksp')
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'io.webfolder:quickjs:1.1.0'
}

View File

@@ -1,13 +1,18 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.8.22'
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
}
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation gradleApi()
implementation 'org.simpleframework:simple-xml:2.7.1'
implementation 'com.soywiz.korlibs.korte:korte-jvm:4.0.9'
implementation 'com.soywiz.korlibs.korte:korte-jvm:4.0.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
}

View File

@@ -2,6 +2,10 @@ plugins {
id 'org.jetbrains.kotlin.jvm'
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14'
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17'
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.parsers
object ErrorMessages {
const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
"Multiple Content Rating are not supported by this source"
const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
"Filtering by both genres and locale is not supported by this source"
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =
"Filtering by both genres and states is not supported by this source"
const val SEARCH_NOT_SUPPORTED = "Search is not supported by this source"
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
@@ -33,16 +34,30 @@ abstract class MangaParser @InternalParsersApi constructor(
open val availableStates: Set<MangaState>
get() = emptySet()
open val availableContentRating: Set<ContentRating>
get() = emptySet()
/**
* Whether parser supports filtering by more than one tag
*/
open val isMultipleTagsSupported: Boolean = true
/**
* Whether parser supports tagsExclude field in filter
*/
open val isTagsExclusionSupported: Boolean = false
/**
* Whether parser supports searching by string query using [MangaListFilter.Search]
*/
open val isSearchSupported: Boolean = true
@Deprecated(
message = "Use availableSortOrders instead",
replaceWith = ReplaceWith("availableSortOrders"),
)
val sortOrders: Set<SortOrder>
open val sortOrders: Set<SortOrder>
get() = availableSortOrders
val config by lazy { context.getConfig(source) }
@@ -67,7 +82,8 @@ abstract class MangaParser @InternalParsersApi constructor(
/**
* Used as fallback if value of `sortOrder` passed to [getList] is null
*/
protected open val defaultSortOrder: SortOrder
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }
@@ -91,12 +107,13 @@ abstract class MangaParser @InternalParsersApi constructor(
"Use getList with filter instead",
replaceWith = ReplaceWith("getList(offset, filter)"),
)
abstract suspend fun getList(
open suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga>
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
/**
* Parse list of manga with search by text query
@@ -130,18 +147,51 @@ abstract class MangaParser @InternalParsersApi constructor(
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
open suspend fun getList(
offset: Int,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder?,
): List<Manga> {
return getList(
offset,
MangaListFilter.Advanced(sortOrder ?: defaultSortOrder, tags.orEmpty(), null, emptySet()),
MangaListFilter.Advanced(
sortOrder = sortOrder ?: defaultSortOrder,
tags = tags.orEmpty(),
tagsExclude = tagsExclude.orEmpty(),
locale = null,
states = emptySet(),
contentRating = emptySet(),
),
)
}
@Suppress("DEPRECATION")
open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getList(offset, null, filter.tags, filter.sortOrder)
is MangaListFilter.Search -> getList(offset, filter.query, null, defaultSortOrder)
null -> getList(offset, null, null, defaultSortOrder)
is MangaListFilter.Advanced -> getList(
offset = offset,
query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getList(
offset = offset,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getList(
offset = offset,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
}
}

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.RestrictTo
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.Paginator
@InternalParsersApi
@@ -21,12 +18,16 @@ abstract class PagedMangaParser(
@JvmField
protected val searchPaginator = Paginator(searchPageSize)
override suspend fun getList(offset: Int, query: String): List<Manga> {
return getList(searchPaginator, offset, query, null, defaultSortOrder)
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
return getList(paginator, offset, null, tags, sortOrder ?: defaultSortOrder)
final override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return getList(
paginator = if (filter is MangaListFilter.Search) {
searchPaginator
} else {
paginator
},
offset = offset,
filter = filter,
)
}
@InternalParsersApi
@@ -35,20 +36,53 @@ abstract class PagedMangaParser(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
abstract suspend fun getListPage(page: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga>
open suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getListPage(
page = page,
query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getListPage(
page = page,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getListPage(
page = page,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
}
}
private suspend fun getList(
paginator: Paginator,
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
filter: MangaListFilter?,
): List<Manga> {
val page = paginator.getPage(offset)
val list = getListPage(page, query, tags, sortOrder)
val list = getListPage(page, filter)
paginator.onListReceived(offset, page, list.size)
return list
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.parsers.model
enum class ContentRating {
SAFE,
SUGGESTIVE,
ADULT
}

View File

@@ -10,9 +10,13 @@ class MangaChapter(
*/
@JvmField val name: String,
/**
* Chapter number starting from 1
* Chapter number starting from 1, 0 if unknown
*/
@JvmField val number: Int,
@JvmField val number: Float,
/**
* Volume number starting from 1, 0 if unknown
*/
@JvmField val volume: Int,
/**
* Relative url to chapter (**without** a domain) or any other uri.
* Used principally in parsers
@@ -32,11 +36,29 @@ class MangaChapter(
*/
@JvmField val branch: String?,
@JvmField val source: MangaSource,
) : Comparable<MangaChapter> {
) {
override fun compareTo(other: MangaChapter): Int {
return number.compareTo(other.number)
}
@Deprecated(message = "Consider using constructor with volume value")
constructor(
id: Long,
name: String,
number: Int,
url: String,
scanlator: String?,
uploadDate: Long,
branch: String?,
source: MangaSource,
) : this(
id = id,
name = name,
number = number.toFloat(),
volume = 0,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -47,6 +69,7 @@ class MangaChapter(
if (id != other.id) return false
if (name != other.name) return false
if (number != other.number) return false
if (volume != other.volume) return false
if (url != other.url) return false
if (scanlator != other.scanlator) return false
if (uploadDate != other.uploadDate) return false
@@ -59,7 +82,8 @@ class MangaChapter(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + number
result = 31 * result + number.hashCode()
result = 31 * result + volume
result = 31 * result + url.hashCode()
result = 31 * result + (scanlator?.hashCode() ?: 0)
result = 31 * result + uploadDate.hashCode()
@@ -71,4 +95,16 @@ class MangaChapter(
override fun toString(): String {
return "MangaChapter($id - #$number [$url] - $source)"
}
internal fun copy(volume: Int, number: Float) = MangaChapter(
id = id,
name = name,
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
import java.util.*
sealed interface MangaListFilter {
@@ -8,6 +9,16 @@ sealed interface MangaListFilter {
val sortOrder: SortOrder?
fun isValid(parser: MangaParser): Boolean = when (this) {
is Advanced -> (sortOrder in parser.availableSortOrders) &&
(tags.size <= 1 || parser.isMultipleTagsSupported) &&
(tagsExclude.isEmpty() || parser.isTagsExclusionSupported) &&
(contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) &&
(states.isEmpty() || parser.availableStates.containsAll(states))
is Search -> parser.isSearchSupported
}
data class Search(
@JvmField val query: String,
) : MangaListFilter {
@@ -20,10 +31,63 @@ sealed interface MangaListFilter {
data class Advanced(
override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>,
@JvmField val tagsExclude: Set<MangaTag>,
@JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>,
@JvmField val contentRating: Set<ContentRating>,
) : MangaListFilter {
override fun isEmpty(): Boolean = tags.isEmpty() && locale == null && states.isEmpty()
override fun isEmpty(): Boolean =
tags.isEmpty() && tagsExclude.isEmpty() && locale == null && states.isEmpty() && contentRating.isEmpty()
fun newBuilder() = Builder(sortOrder)
.tags(tags)
.tagsExclude(tagsExclude)
.locale(locale)
.states(states)
.contentRatings(contentRating)
class Builder(sortOrder: SortOrder) {
private var _sortOrder: SortOrder = sortOrder
private var _tags: Set<MangaTag>? = null
private var _tagsExclude: Set<MangaTag>? = null
private var _locale: Locale? = null
private var _states: Set<MangaState>? = null
private var _contentRating: Set<ContentRating>? = null
fun sortOrder(order: SortOrder) = apply {
_sortOrder = order
}
fun tags(tags: Set<MangaTag>?) = apply {
_tags = tags
}
fun tagsExclude(tags: Set<MangaTag>?) = apply {
_tagsExclude = tags
}
fun locale(locale: Locale?) = apply {
_locale = locale
}
fun states(states: Set<MangaState>?) = apply {
_states = states
}
fun contentRatings(rating: Set<ContentRating>?) = apply {
_contentRating = rating
}
fun build() = Advanced(
sortOrder = _sortOrder,
tags = _tags.orEmpty(),
tagsExclude = _tagsExclude.orEmpty(),
locale = _locale,
states = _states.orEmpty(),
contentRating = _contentRating.orEmpty(),
)
}
}
}

View File

@@ -1,5 +1,5 @@
package org.koitharu.kotatsu.parsers.model
enum class MangaState {
ONGOING, FINISHED, ABANDONED, PAUSED
ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING
}

View File

@@ -5,5 +5,6 @@ enum class SortOrder {
POPULARITY,
RATING,
NEWEST,
ALPHABETICAL
}
ALPHABETICAL,
ALPHABETICAL_DESC
}

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.isNullOrEmpty
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
@@ -33,6 +34,12 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.ALPHABETICAL,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val isTagsExclusionSupported: Boolean = true
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override val configKeyDomain = ConfigKey.Domain(
"bato.to",
"batocomic.com",
@@ -61,37 +68,85 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.org",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
return search(page, query)
}
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (sortOrder) {
SortOrder.UPDATED,
-> append("update.za")
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
SortOrder.RATING -> Unit
when (filter) {
is MangaListFilter.Search -> {
return search(page, filter.query)
}
if (!tags.isNullOrEmpty()) {
append("&genres=")
appendAll(tags, ",") { it.key }
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (filter.sortOrder) {
SortOrder.UPDATED -> append("update.za")
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
else -> append("update.za")
}
filter.states.oneOrThrowIfMany()?.let {
append("&release=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "cancelled"
MangaState.PAUSED -> "hiatus"
MangaState.UPCOMING -> "pending"
},
)
}
filter.locale?.let {
append("&langs=")
append(it.language)
}
append("&genres=")
if (filter.tags.isNotEmpty()) {
appendAll(filter.tags, ",") { it.key }
}
append("|")
if (filter.tagsExclude.isNotEmpty()) {
appendAll(filter.tagsExclude, ",") { it.key }
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.oneOrThrowIfMany()?.let {
append(
when (it) {
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
else -> append("")
},
)
}
}
append("&page=")
append(page.toString())
}
return parseList(url, page)
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=update.za")
append("&page=")
append(page.toString())
}
return parseList(url, page)
}
append("&page=")
append(page)
}
return parseList(url, page)
}
override suspend fun getDetails(manga: Manga): Manga {
@@ -109,9 +164,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
?.selectFirst(".limit-html")
?.html(),
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
state = when (attrs["Release status:"]?.text()) {
state = when (attrs["Original work:"]?.text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Cancelled" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> manga.state
},
author = attrs["Authors:"]?.text()?.trim() ?: manga.author,
@@ -129,7 +186,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
val scripts = webClient.httpGet(fullUrl).parseHtml().select("script")
for (script in scripts) {
val scriptSrc = script.html()
val p = scriptSrc.indexOf("const imgHttpLis =")
val p = scriptSrc.indexOf("const imgHttps =")
if (p == -1) continue
val start = scriptSrc.indexOf('[', p)
val end = scriptSrc.indexOf(';', start)
@@ -149,7 +206,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
val url = images.getString(i)
result += MangaPage(
id = generateUid(url),
url = url + "?" + args.getString(i),
url = if (args.length() == 0) url else url + "?" + args.getString(i),
preview = null,
source = source,
)
@@ -170,7 +227,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
jo.keys().forEach { key ->
val item = jo.getJSONObject(key)
result += MangaTag(
title = item.getString("text").toTitleCase(),
title = item.getString("text").toTitleCase(Locale.ENGLISH),
key = item.getString("file"),
source = source,
)
@@ -180,6 +237,22 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find gernes list", scripts[0].baseUri())
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE,
Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"),
Locale("bn"), Locale("bs"), Locale("bg"), Locale("my"), Locale("km"),
Locale("ceb"), Locale("zh_hk"), Locale("zh_tw"), Locale("hr"), Locale("cs"),
Locale("da"), Locale("nl"), Locale("eo"), Locale("et"), Locale("fil"),
Locale("fi"), Locale("ka"), Locale("el"), Locale("ht"), Locale("he"),
Locale("hi"), Locale("hu"), Locale("id"), Locale("kk"), Locale("ko"),
Locale("lv"), Locale("ms"), Locale("ml"), Locale("mo"), Locale("mn"),
Locale("ne"), Locale("no"), Locale("fa"), Locale("pl"), Locale("pt"),
Locale("pt_br"), Locale("pt_pt"), Locale("ro"), Locale("ru"), Locale("sr"),
Locale("si"), Locale("sk"), Locale("es"), Locale("es_419"), Locale("ta"),
Locale("te"), Locale("th"), Locale("ti"), Locale("tr"), Locale("uk"),
Locale("vi"), Locale("zu"),
)
private suspend fun search(page: Int, query: String): List<Manga> {
val url = buildString {
append("https://")

View File

@@ -2,10 +2,11 @@ package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@@ -14,16 +15,15 @@ import java.text.SimpleDateFormat
import java.util.*
/**
* https://api.comick.fun/docs/static/index.html
* cc
*/
private const val PAGE_SIZE = 20
private const val CHAPTERS_LIMIT = 99999
@MangaSourceParser("COMICK_FUN", "ComicK")
internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.COMICK_FUN) {
internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.COMICK_FUN, 20) {
override val configKeyDomain = ConfigKey.Domain("comick.app")
override val configKeyDomain = ConfigKey.Domain("comick.cc")
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
@@ -31,47 +31,58 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
SortOrder.RATING,
)
@Volatile
private var cachedTags: SparseArrayCompat<MangaTag>? = null
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
private val tagsArray = SuspendLazy(::loadTags)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val domain = domain
val url = buildString {
append("https://api.")
append(domain)
append("/v1.0/search?tachiyomi=true")
if (!query.isNullOrEmpty()) {
if (offset > 0) {
return emptyList()
val url = urlBuilder()
.host("api.$domain")
.addPathSegment("v1.0")
.addPathSegment("search")
.addQueryParameter("type", "comic")
.addQueryParameter("tachiyomi", "true")
.addQueryParameter("limit", pageSize.toString())
.addQueryParameter("page", page.toString())
when (filter) {
is MangaListFilter.Search -> {
url.addQueryParameter("q", filter.query)
}
null -> {
url.addQueryParameter("sort", "view")
}
is MangaListFilter.Advanced -> {
filter.tags.forEach { tag ->
url.addQueryParameter("genres", tag.key)
}
append("&q=")
append(query.urlEncoded())
} else {
append("&limit=")
append(PAGE_SIZE)
append("&page=")
append((offset / PAGE_SIZE) + 1)
if (!tags.isNullOrEmpty()) {
append("&genres=")
appendAll(tags, "&genres=", MangaTag::key)
}
append("&sort=") // view, uploaded, rating, follow, user_follow_count
append(
when (sortOrder) {
url.addQueryParameter(
"sort",
when (filter.sortOrder) {
SortOrder.POPULARITY -> "view"
SortOrder.RATING -> "rating"
else -> "uploaded"
},
)
filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter(
"status",
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.ABANDONED -> "3"
MangaState.PAUSED -> "4"
else -> ""
},
)
}
}
}
val ja = webClient.httpGet(url).parseJsonArray()
val tagsMap = cachedTags ?: loadTags()
val ja = webClient.httpGet(url.build()).parseJsonArray()
val tagsMap = tagsArray.get()
return ja.mapJSON { jo ->
val slug = jo.getString("slug")
Manga(
@@ -86,13 +97,13 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
largeCoverUrl = null,
description = jo.getStringOrNull("desc"),
tags = jo.selectGenres(tagsMap),
state = runCatching {
if (jo.getBoolean("translation_completed")) {
MangaState.FINISHED
} else {
MangaState.ONGOING
}
}.getOrNull(),
state = when (jo.getIntOrDefault("status", 0)) {
1 -> MangaState.ONGOING
2 -> MangaState.FINISHED
3 -> MangaState.ABANDONED
4 -> MangaState.PAUSED
else -> null
},
author = null,
source = source,
)
@@ -104,11 +115,12 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
val jo = webClient.httpGet(url).parseJson()
val comic = jo.getJSONObject("comic")
var alt = ""
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy(
title = comic.getString("title"),
altTitle = null, // TODO
altTitle = alt.ifEmpty { comic.getStringOrNull("title") },
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
description = comic.getStringOrNull("parsed") ?: comic.getString("desc"),
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
val g = it.getJSONObject("md_genres")
MangaTag(
@@ -138,7 +150,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val sparseArray = cachedTags ?: loadTags()
val sparseArray = tagsArray.get()
val set = ArraySet<MangaTag>(sparseArray.size())
for (i in 0 until sparseArray.size()) {
set.add(sparseArray.valueAt(i))
@@ -153,13 +165,12 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
tags.append(
jo.getInt("id"),
MangaTag(
title = jo.getString("name"),
title = jo.getString("name").toTitleCase(Locale.ENGLISH),
key = jo.getString("slug"),
source = source,
),
)
}
cachedTags = tags
return tags
}
@@ -168,9 +179,39 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
url = "https://api.${domain}/comic/$hid/chapters?limit=$CHAPTERS_LIMIT",
).parseJson().getJSONArray("chapters")
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
val list = ja.toJSONList().reversed()
val counters = HashMap<String?, Int>()
return ja.toJSONList().reversed().mapChapters { _, jo ->
val vol = jo.getStringOrNull("vol")
val chap = jo.getStringOrNull("chap")
val locale = Locale.forLanguageTag(jo.getString("lang"))
val group = jo.optJSONArray("group_name")?.joinToString(", ")
val branch = buildString {
append(locale.getDisplayName(locale).toTitleCase(locale))
if (!group.isNullOrEmpty()) {
append(" (")
append(group)
append(')')
}
}
MangaChapter(
id = generateUid(jo.getLong("id")),
name = buildString {
vol?.let { append("Vol ").append(it).append(' ') }
chap?.let { append("Chap ").append(it) }
jo.getStringOrNull("title")?.let { append(": ").append(it) }
},
number = counters.incrementAndGet(branch),
url = jo.getString("hid"),
scanlator = jo.optJSONArray("group_name")?.asIterable<String>()?.joinToString()
?.takeUnless { it.isBlank() },
uploadDate = dateFormat.tryParse(jo.getString("created_at").substringBefore('T')),
branch = branch,
source = source,
)
}
val chaptersBuilder = ChaptersListBuilder(list.size)
/*val chaptersBuilder = ChaptersListBuilder(list.size)
val branchedChapters = HashMap<String?, HashMap<Pair<String?, String?>, MangaChapter>>()
for (jo in list) {
val vol = jo.getStringOrNull("vol")
@@ -201,7 +242,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
branchedChapters.getOrPut(branch, ::HashMap)[volChap] = chapter
}
}
return chaptersBuilder.toList()
return chaptersBuilder.toList()*/
}
private fun JSONObject.selectGenres(tags: SparseArrayCompat<MangaTag>): Set<MangaTag> {
@@ -209,9 +250,13 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
val res = ArraySet<MangaTag>(array.length())
for (i in 0 until array.length()) {
val id = array.getInt(i)
val tag = tags.get(id) ?: continue
val tag = tags[id] ?: continue
res.add(tag)
}
return res
}
private fun JSONArray.joinToString(separator: String): String {
return (0 until length()).joinToString(separator) { i -> getString(i) }
}
}

View File

@@ -1,8 +1,11 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat
import androidx.collection.set
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@@ -23,9 +26,8 @@ internal class ExHentaiParser(
context: MangaLoaderContext,
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
override val availableSortOrders: Set<SortOrder> = Collections.singleton(
SortOrder.NEWEST,
)
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val isTagsExclusionSupported: Boolean = true
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain(
@@ -40,7 +42,8 @@ internal class ExHentaiParser(
private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash")
private var updateDm = false
private val nextPages = SparseArrayCompat<Long>()
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(true)
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val tagsMap = SuspendLazy(::fetchTags)
override val isAuthorized: Boolean
get() {
@@ -65,38 +68,54 @@ internal class ExHentaiParser(
paginator.firstPage = 0
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var search = query?.urlEncoded().orEmpty()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val next = nextPages.get(page, 0L)
if (page > 0 && next == 0L) {
assert(false) { "Page timestamp not found" }
return emptyList()
}
var search = ""
val url = buildString {
append("https://")
append(domain)
append("/?next=")
append(next)
if (!tags.isNullOrEmpty()) {
var fCats = 0
for (tag in tags) {
tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
search += tag.key + " "
when (filter) {
is MangaListFilter.Search -> {
search += filter.query.urlEncoded()
append("&f_search=")
append(search.trim().replace(' ', '+'))
}
is MangaListFilter.Advanced -> {
filter.toSearchQuery()?.let { sq ->
append("&f_search=")
append(sq.urlEncoded())
}
val catsOn = filter.tags.mapNotNullToSet { it.key.toIntOrNull() }
val catsOff = filter.tagsExclude.mapNotNullToSet { it.key.toIntOrNull() }
if (catsOff.size >= 10) {
return emptyList()
}
var fCats = catsOn.fold(0, Int::or)
if (fCats != 0) {
fCats = 1023 - fCats
}
fCats = catsOff.fold(fCats, Int::or)
if (fCats != 0) {
append("&f_cats=")
append(fCats)
}
}
if (fCats != 0) {
append("&f_cats=")
append(1023 - fCats)
}
}
if (search.isNotEmpty()) {
append("&f_search=")
append(search.trim().replace(' ', '+'))
null -> {}
}
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
if (updateDm) {
@@ -107,6 +126,7 @@ internal class ExHentaiParser(
append("&f_sh=on")
}
}
val body = webClient.httpGet(url).parseHtml().body()
val root = body.selectFirst("table.itg")
?.selectFirst("tbody")
@@ -114,17 +134,17 @@ internal class ExHentaiParser(
body.parseFailed("Cannot find root")
} else {
updateDm = true
return getListPage(page, query, tags, sortOrder)
return getListPage(page, filter)
}
updateDm = false
nextPages[page + 1] = getNextTimestamp(body)
return root.children().mapNotNull { tr ->
if (tr.childrenSize() != 2) return@mapNotNull null
val (td1, td2) = tr.children()
val glink = td2.selectFirstOrThrow("div.glink")
val a = glink.parents().select("a").first() ?: glink.parseFailed("link not found")
val gLink = td2.selectFirstOrThrow("div.glink")
val a = gLink.parents().select("a").first() ?: gLink.parseFailed("link not found")
val href = a.attrAsRelativeUrl("href")
val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
val tagsDiv = gLink.nextElementSibling() ?: gLink.parseFailed("tags div not found")
val mainTag = td2.selectFirst("div.cn")?.let { div ->
MangaTag(
title = div.text().toTitleCase(),
@@ -134,7 +154,7 @@ internal class ExHentaiParser(
}
Manga(
id = generateUid(href),
title = glink.text().cleanupTitle(),
title = gLink.text().cleanupTitle(),
altTitle = null,
url = href,
publicUrl = a.absUrl("href"),
@@ -155,8 +175,17 @@ internal class ExHentaiParser(
val root = doc.body().selectFirstOrThrow("div.gm")
val cover = root.getElementById("gd1")?.children()?.first()
val title = root.getElementById("gd2")
val taglist = root.getElementById("taglist")
val tagList = root.getElementById("taglist")
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
val lang = root.getElementById("gd3")
?.selectFirst("tr:contains(Language)")
?.selectFirst(".gdt2")?.ownTextOrNull()
val tagMap = tagsMap.get()
val tags = ArraySet<MangaTag>()
tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
return manga.copy(
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle,
@@ -166,10 +195,11 @@ internal class ExHentaiParser(
?.toFloatOrNull()
?.div(5f) ?: manga.rating,
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
description = taglist?.select("tr")?.joinToString("<br>") { tr ->
tags = tags,
description = tagList?.select("tr")?.joinToString("<br>") { tr ->
val (tc, td) = tr.children()
val subtags = td.select("a").joinToString { it.html() }
"<b>${tc.html()}</b> $subtags"
val subTags = td.select("a").joinToString { it.html() }
"<b>${tc.html()}</b> $subTags"
},
chapters = tabs?.select("a")?.findLast { a ->
a.text().toIntOrNull() != null
@@ -186,7 +216,7 @@ internal class ExHentaiParser(
uploadDate = 0L,
source = source,
scanlator = null,
branch = null,
branch = lang,
)
}
chapters.toList()
@@ -213,18 +243,72 @@ internal class ExHentaiParser(
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
}
private val tags =
"ahegao,anal,angel,apron,bandages,bbw,bdsm,beauty mark,big areolae,big ass,big breasts,big clit,big lips," +
"big nipples,bikini,blackmail,bloomers,blowjob,bodysuit,bondage,breast expansion,bukkake,bunny girl,business suit," +
"catgirl,centaur,cheating,chinese dress,christmas,collar,corset,cosplaying,cowgirl,crossdressing,cunnilingus," +
"dark skin,daughter,deepthroat,defloration,demon girl,double penetration,dougi,dragon,drunk,elf,exhibitionism,farting," +
"females only,femdom,filming,fingering,fishnets,footjob,fox girl,furry,futanari,garter belt,ghost,giantess," +
"glasses,gloves,goblin,gothic lolita,growth,guro,gyaru,hair buns,hairy,hairy armpits,handjob,harem,hidden sex," +
"horns,huge breasts,humiliation,impregnation,incest,inverted nipples,kemonomimi,kimono,kissing,lactation," +
"latex,leg lock,leotard,lingerie,lizard girl,maid,masked face,masturbation,midget,miko,milf,mind break," +
"mind control,monster girl,mother,muscle,nakadashi,netorare,nose hook,nun,nurse,oil,paizuri,panda girl," +
"pantyhose,piercing,pixie cut,policewoman,ponytail,pregnant,rape,rimjob,robot,scat,lolicon,schoolgirl uniform," +
"sex toys,shemale,sister,small breasts,smell,sole dickgirl,sole female,squirting,stockings,sundress,sweating," +
"swimsuit,swinging,tail,tall girl,teacher,tentacles,thigh high boots,tomboy,transformation,twins,twintails," +
"unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," +
"tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis"
override suspend fun getAvailableTags(): Set<MangaTag> {
return tagsMap.get().values.toSet()
}
private suspend fun fetchTags(): Map<String, MangaTag> {
val tagMap = ArrayMap<String, MangaTag>()
val tagElements = tags.split(",")
for (el in tagElements) {
if (el.isEmpty()) continue
tagMap[el] = MangaTag(
title = el.toTitleCase(Locale.ENGLISH),
key = el,
source = source,
)
}
val doc = webClient.httpGet("https://${domain}").parseHtml()
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
return root.select("div.cs").mapNotNullToSet { div ->
val id = div.id().substringAfterLast('_').toIntOrNull()
?: return@mapNotNullToSet null
MangaTag(
title = div.text().toTitleCase(),
root.select("div.cs").mapNotNullToSet { div ->
val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null
val name = div.text().toTitleCase(Locale.ENGLISH)
tagMap[name] = MangaTag(
title = "Kind: $name",
key = id.toString(),
source = source,
)
}
return tagMap
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.JAPANESE,
Locale.ENGLISH,
Locale.CHINESE,
Locale("nl"),
Locale.FRENCH,
Locale.GERMAN,
Locale("hu"),
Locale.ITALIAN,
Locale("kr"),
Locale("pl"),
Locale("pt"),
Locale("ru"),
Locale("es"),
Locale("th"),
Locale("vi"),
)
private fun Locale.toLanguagePath() = when (language) {
else -> getDisplayLanguage(Locale.ENGLISH).lowercase()
}
override suspend fun getUsername(): String {
@@ -307,4 +391,30 @@ internal class ExHentaiParser(
?.queryParameter("next")
?.toLongOrNull() ?: 1
}
private fun MangaListFilter.Advanced.toSearchQuery(): String? {
val joiner = StringUtil.StringJoiner(" ")
for (tag in tags) {
if (tag.key.isNumeric()) {
continue
}
joiner.add("tag:\"")
joiner.append(tag.key)
joiner.append("\"$")
}
for (tag in tagsExclude) {
if (tag.key.isNumeric()) {
continue
}
joiner.add("-tag:\"")
joiner.append(tag.key)
joiner.append("\"$")
}
locale?.let { lc ->
joiner.add("language:\"")
joiner.append(lc.toLanguagePath())
joiner.append("\"$")
}
return joiner.complete().takeUnless { it.isEmpty() }
}
}

View File

@@ -0,0 +1,706 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Headers
import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.Jsoup
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class)
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HITOMILA) {
override val configKeyDomain = ConfigKey.Domain("hitomi.la")
private val ltnBaseUrl get() = "https://${getDomain("ltn")}"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.POPULARITY,
)
private val localeMap: Map<Locale, String> = mapOf(
Locale("id") to "indonesian",
Locale("jv") to "javanese",
Locale("ca") to "catalan",
Locale("ceb") to "cebuano",
Locale("cs") to "czech",
Locale("da") to "danish",
Locale("de") to "german",
Locale("et") to "estonian",
Locale.ENGLISH to "english",
Locale("es") to "spanish",
Locale("eo") to "esperanto",
Locale("fr") to "french",
Locale("it") to "italian",
Locale("hi") to "hindi",
Locale("hu") to "hungarian",
Locale("pl") to "polish",
Locale("pt") to "portuguese",
Locale("vi") to "vietnamese",
Locale("tr") to "turkish",
Locale("ru") to "russian",
Locale("uk") to "ukrainian",
Locale("ar") to "arabic",
Locale.KOREAN to "korean",
Locale.CHINESE to "chinese",
Locale.JAPANESE to "japanese",
)
private fun Locale?.getSiteLang(): String = when (this) {
null -> "all"
else -> localeMap[this] ?: "all"
}
override suspend fun getAvailableLocales(): Set<Locale> = localeMap.keys
override suspend fun getAvailableTags(): Set<MangaTag> = coroutineScope {
('a'..'z').map { alphabet ->
async {
val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml()
doc.select(".posts > li").mapNotNull { element ->
val num =
element.ownText().let {
Regex("""\((\d+)\)""").find(it)?.groupValues?.get(1)?.toIntOrNull() ?: 0
}
if (num > 100) {
val url = element.selectFirst("a")
val href =
url?.attrAsRelativeUrl("href")
?: return@mapNotNull null
MangaTag(
title = url.ownText().toTagTitle(),
key = href.tagUrlToTag(),
source = source,
)
} else {
null
}
}
}
}.awaitAll().flatten().toSet()
}
private var cachedSearchIds: List<Int> = emptyList()
override suspend fun getList(
offset: Int,
filter: MangaListFilter?,
): List<Manga> = when (filter) {
is MangaListFilter.Advanced -> {
if (filter.tags.isEmpty()) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> {
getGalleryIDsFromNozomi(
"popular",
"today",
filter.locale.getSiteLang(),
offset.nextOffsetRange(),
)
}
else -> {
getGalleryIDsFromNozomi(null, "index", filter.locale.getSiteLang(), offset.nextOffsetRange())
}
}
} else {
if (offset == 0) {
cachedSearchIds =
hitomiSearch(
filter.tags.joinToString(" ") { it.key },
filter.sortOrder == SortOrder.POPULARITY,
filter.locale.getSiteLang(),
).toList()
}
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
}
}
is MangaListFilter.Search -> {
if (offset == 0) {
cachedSearchIds = hitomiSearch(filter.query, filter.sortOrder == SortOrder.POPULARITY).toList()
}
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
}
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
}.toMangaList()
private fun Int.nextOffsetRange(): LongRange {
val bytes = this * 4L
return bytes.until(bytes + 100L)
}
private suspend fun hitomiSearch(
query: String,
sortByPopularity: Boolean = false,
language: String = "all",
): Set<Int> =
coroutineScope {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
for (term in terms) {
if (term.startsWith("-")) {
negativeTerms.push(term.removePrefix("-"))
} else if (term.isNotBlank()) {
positiveTerms.push(term)
}
}
val positiveResults = positiveTerms.map {
async {
runCatchingCancellable {
getGalleryIDsForQuery(it, language)
}.getOrDefault(emptySet())
}
}
val negativeResults = negativeTerms.map {
async {
runCatchingCancellable {
getGalleryIDsForQuery(it, language)
}.getOrDefault(emptySet())
}
}
val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language)
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language)
else -> emptySet()
}.toMutableSet()
fun filterPositive(newResults: Set<Int>) {
when {
results.isEmpty() -> results.addAll(newResults)
else -> results.retainAll(newResults)
}
}
fun filterNegative(newResults: Set<Int>) {
results.removeAll(newResults)
}
// positive results
positiveResults.forEach {
filterPositive(it.await())
}
// negative results
negativeResults.forEach {
filterNegative(it.await())
}
results
}
// search.js
private suspend fun getGalleryIDsForQuery(
query: String,
language: String = "all",
): Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
val sides = it.split(":")
val ns = sides[0]
var tag = sides[1]
var area: String? = ns
var lang = language
when (ns) {
"female", "male" -> {
area = "tag"
tag = it
}
"language" -> {
area = null
lang = tag
tag = "index"
}
}
return getGalleryIDsFromNozomi(area, tag, lang)
}
val key = hashTerm(it)
val node = getGalleryNodeAtAddress(0)
val data = bSearch(key, node) ?: return emptySet()
return getGalleryIDsFromData(data)
}
}
private suspend fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.data"
val (offset, length) = data
require(length in 1..100000000) {
"Length $length is too long"
}
val inbuf = getRangedResponse(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>()
val buffer =
ByteBuffer
.wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs * 4 + 4
require(numberOfGalleryIDs in 1..10000000) {
"number_of_galleryids $numberOfGalleryIDs is too long"
}
require(inbuf.size == expectedLength) {
"inbuf.byteLength ${inbuf.size} != expected_length $expectedLength"
}
for (i in 0.until(numberOfGalleryIDs))
galleryIDs.add(buffer.int)
return galleryIDs
}
private suspend fun bSearch(
key: UByteArray,
node: Node,
): Pair<Long, Int>? {
fun compareArrayBuffers(
dv1: UByteArray,
dv2: UByteArray,
): Int {
val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) {
if (dv1[i] < dv2[i]) {
return -1
} else if (dv1[i] > dv2[i]) {
return 1
}
}
return 0
}
fun locateKey(
key: UByteArray,
node: Node,
): Pair<Boolean, Int> {
for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0) {
return Pair(cmpResult == 0, i)
}
}
return Pair(false, node.keys.size)
}
fun isLeaf(node: Node): Boolean {
for (subnode in node.subNodeAddresses)
if (subnode != 0L) {
return false
}
return true
}
if (node.keys.isEmpty()) {
return null
}
val (there, where) = locateKey(key, node)
if (there) {
return node.datas[where]
} else if (isLeaf(node)) {
return null
}
val nextNode = getGalleryNodeAtAddress(node.subNodeAddresses[where])
return bSearch(key, nextNode)
}
private suspend fun getGalleryIDsFromNozomi(
area: String?,
tag: String,
language: String,
range: LongRange? = null,
): Set<Int> {
val nozomiAddress = when (area) {
null -> "$ltnBaseUrl/$tag-$language.nozomi"
else -> "$ltnBaseUrl/$area/$tag-$language.nozomi"
}
val bytes = getRangedResponse(nozomiAddress, range)
val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return nozomi
}
private val galleriesIndexVersion = SuspendLazy {
webClient.httpGet("$ltnBaseUrl/galleriesindex/version?_=${System.currentTimeMillis()}").parseRaw()
}
private data class Node(
val keys: List<UByteArray>,
val datas: List<Pair<Long, Int>>,
val subNodeAddresses: List<Long>,
)
private fun decodeNode(data: ByteArray): Node {
val buffer = ByteBuffer
.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
val uData = data.toUByteArray()
val numberOfKeys = buffer.int
val keys = ArrayList<UByteArray>()
for (i in 0.until(numberOfKeys)) {
val keySize = buffer.int
if (keySize == 0 || keySize > 32) {
throw Exception("fatal: !keySize || keySize > 32")
}
keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
buffer.position(buffer.position() + keySize)
}
val numberOfDatas = buffer.int
val datas = ArrayList<Pair<Long, Int>>()
for (i in 0.until(numberOfDatas)) {
val offset = buffer.long
val length = buffer.int
datas.add(Pair(offset, length))
}
val numberOfSubNodeAddresses = 16 + 1
val subNodeAddresses = ArrayList<Long>()
for (i in 0.until(numberOfSubNodeAddresses)) {
val subNodeAddress = buffer.long
subNodeAddresses.add(subNodeAddress)
}
return Node(keys, datas, subNodeAddresses)
}
private suspend fun getGalleryNodeAtAddress(address: Long): Node {
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.index"
val nodedata = getRangedResponse(url, address.until(address + 464))
return decodeNode(nodedata)
}
private suspend fun getRangedResponse(
url: String,
range: LongRange? = null,
): ByteArray {
val rangeHeaders = when (range) {
null -> Headers.headersOf()
else -> Headers.headersOf("Range", "bytes=${range.first}-${range.last}")
}
return webClient.httpGet(url, rangeHeaders).parseBytes()
}
private fun hashTerm(term: String): UByteArray {
return sha256(term.toByteArray()).copyOfRange(0, 4).toUByteArray()
}
private fun sha256(data: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data)
}
private suspend fun Collection<Int>.toMangaList(): List<Manga> = coroutineScope {
map { id ->
async {
runCatchingCancellable {
val doc = webClient.httpGet("$ltnBaseUrl/galleryblock/$id.html").let {
val baseUri = it.request.url.toString()
val html = it.parseRaw()
Jsoup.parse(rewriteTnPaths(html), baseUri)
}
Manga(
id = generateUid(id.toString()),
title = doc.selectFirstOrThrow("h1").text(),
url = id.toString(),
coverUrl =
"https:" +
doc.selectFirstOrThrow("picture > source")
.attr("data-srcset")
.substringBefore(" "),
publicUrl =
doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href")
.toAbsoluteUrl(domain),
author = null,
tags = emptySet(),
isNsfw = true,
rating = RATING_UNKNOWN,
altTitle = null,
state = null,
source = source,
)
}.getOrNull()
}
}.awaitAll().filterNotNull()
}
override suspend fun getDetails(manga: Manga): Manga {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js")
.parseRaw()
.substringAfter("var galleryinfo = ")
.let(::JSONObject)
return manga.copy(
title = json.getString("title"),
largeCoverUrl =
json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash")
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
},
author =
json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString(),
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
tags =
buildSet {
json.optJSONArray("characters")
?.mapToTags("character")
?.let(::addAll)
json.optJSONArray("tags")
?.mapToTags("tag")
?.let(::addAll)
json.optJSONArray("artists")
?.mapToTags("artist")
?.let(::addAll)
json.optJSONArray("parodys")
?.mapToTags("parody")
?.let(::addAll)
json.optJSONArray("groups")
?.mapToTags("group")
?.let(::addAll)
},
chapters = listOf(
MangaChapter(
id = generateUid(manga.url),
url = manga.url,
name = json.getString("title"),
scanlator = json.getString("type").toTitleCase(),
number = 1,
branch = json.getString("language_localname"),
source = source,
uploadDate = dateFormat.tryParse(json.getString("date").substringBeforeLast("-")),
),
),
)
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
private fun JSONArray.mapToTags(key: String): Set<MangaTag> {
val tags = ArraySet<MangaTag>(length())
mapJSON {
MangaTag(
title =
it.getString(key).toCamelCase().let { title ->
if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
"$title ♀"
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
"$title ♂"
} else {
title
}
},
key = it.getString("url").tagUrlToTag(),
source = source,
).let(tags::add)
}
return tags
}
private fun String.tagUrlToTag(): String {
val urlContent = this.split("/")
val ns = urlContent[1]
val tag =
urlContent[2]
.substringBeforeLast("-")
.urlDecode()
.replace(" ", "_")
return if (tag.split(":")[0] in listOf("female", "male")) {
tag
} else {
"$ns:$tag"
}
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js")
.parseRaw()
.substringAfter("var galleryinfo = ")
.let(::JSONObject)
// any better way to get List<Int> from this json?
return json.getJSONArray("related").let {
0.until(it.length()).map { i -> it.getInt(i) }
}.toMangaList()
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val json = webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js")
.parseRaw()
.substringAfter("var galleryinfo = ")
.let(::JSONObject)
return json.getJSONArray("files").mapJSON { image ->
val hash = image.getString("hash")
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
MangaPage(
id = generateUid(hash),
url = "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp",
preview = "https://${getDomain("${subDomain}tn")}/webpsmalltn/${thumbPathFromHash(hash)}/$hash.webp",
source = source,
)
}
}
// / --->
private var scriptLastRetrieval: Long? = null
private val mutex = Mutex()
private var subdomainOffsetDefault = 0
private val subdomainOffsetMap = mutableMapOf<Int, Int>()
private var commonImageId = ""
private suspend fun refreshScript() = mutex.withLock {
if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) {
val ggScript = webClient.httpGet("$ltnBaseUrl/gg.js?_=${System.currentTimeMillis()}").parseRaw()
subdomainOffsetDefault = Regex("var o = (\\d)").find(ggScript)!!.groupValues[1].toInt()
val o = Regex("o = (\\d); break;").find(ggScript)!!.groupValues[1].toInt()
subdomainOffsetMap.clear()
Regex("case (\\d+):").findAll(ggScript).forEach {
val case = it.groupValues[1].toInt()
subdomainOffsetMap[case] = o
}
commonImageId = Regex("b: '(.+)'").find(ggScript)!!.groupValues[1]
scriptLastRetrieval = System.currentTimeMillis()
}
}
// m <-- gg.js
private suspend fun subdomainOffset(imageId: Int): Int {
refreshScript()
return subdomainOffsetMap[imageId] ?: subdomainOffsetDefault
}
// b <-- gg.js
private suspend fun commonImageId(): String {
refreshScript()
return commonImageId
}
// s <-- gg.js
private fun imageIdFromHash(hash: String): Int {
val match = Regex("(..)(.)$").find(hash)
return match!!.groupValues.let { it[2] + it[1] }.toInt(16)
}
// real_full_path_from_hash <-- common.js
private fun thumbPathFromHash(hash: String): String {
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
}
private suspend fun subdomainFromURL(url: String, base: String? = null): String {
var retval = "b"
if (!base.isNullOrBlank())
retval = base
val regex = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
val hashMatch = regex.find(url) ?: return "a"
val imageId = hashMatch.groupValues.let { it[2] + it[1] }.toIntOrNull(16)
if (imageId != null) {
retval = ('a' + subdomainOffset(imageId)).toString() + retval
}
return retval
}
// rewrite_tn_paths <-- common.js
private suspend fun rewriteTnPaths(html: String): String {
val tnRegex = Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")
val url = tnRegex.find(html)?.value ?: return html
val newSubdomain = subdomainFromURL(url, "tn")
val newUrl = url.replace(Regex("""//..?\.hitomi\.la/"""), "//${getDomain(newSubdomain)}/")
return html.replace(tnRegex, newUrl)
}
private fun String.toTagTitle(): String {
return toCamelCase()
.replace("♂", "(male)")
.replace("♀", "(female)")
}
}

View File

@@ -22,45 +22,45 @@ internal class ImHentai(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("imhentai.xxx")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
append("/search/?key=")
append(query.urlEncoded())
append("&page=")
append(page)
} else if (!tags.isNullOrEmpty()) {
append("/tag/")
append(tag?.key.orEmpty())
append("/")
when (sortOrder) {
SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("popular/")
else -> append("")
append("/search/?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&key=")
append(filter.query.urlEncoded())
}
append("?page=")
append(page)
} else {
append("/search/?page=")
append(page)
when (sortOrder) {
SortOrder.UPDATED -> append("&lt=1&pp=0")
SortOrder.POPULARITY -> append("&lt=0&pp=1")
SortOrder.RATING -> append("&lt=0&pp=0")
else -> append("&lt=1&pp=0")
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("&key=")
append(filter.tags.joinToString(separator = ",") { it.key })
}
var lang = "&en=1&jp=1&es=1&fr=1&kr=1&de=1&ru=1"
filter.locale?.let {
lang = "&en=0&jp=0&es=0&fr=0&kr=0&de=0&ru=0"
lang = lang.replace("${it.language}=0", "${it.language}=1")
}
append(lang)
when (filter.sortOrder) {
SortOrder.UPDATED -> append("&lt=1&pp=0")
SortOrder.POPULARITY -> append("&lt=0&pp=1")
SortOrder.RATING -> append("&lt=0&pp=0")
else -> append("&lt=1&pp=0")
}
}
null -> {
append("&lt=1&pp=0")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.galleries div.thumb").map { div ->
@@ -103,11 +103,15 @@ internal class ImHentai(context: MangaLoaderContext) :
val href = it.attr("href").substringAfterLast("tag/").substringBeforeLast('/')
MangaTag(
key = href,
title = it.selectFirstOrThrow("h3.list_tag").text(),
title = it.selectFirstOrThrow("h3.list_tag").text().toTitleCase(sourceLocale),
source = source,
)
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH, Locale.JAPANESE, Locale("es"), Locale.FRENCH, Locale("kr"), Locale.GERMAN, Locale("ru"),
)
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()

View File

@@ -132,83 +132,117 @@ internal abstract class LineWebtoonsParser(
}
}
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val genre = tags.oneOrThrowIfMany()?.key ?: "ALL"
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val manga =
when (filter) {
is MangaListFilter.Search -> {
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
.getJSONObject("challengeSearch")
.getJSONArray("titleList")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
val sortOrderStr = when (sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
}
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = emptySet(),
author = jo.getStringOrNull("writingAuthorName"),
description = null,
state = null,
source = source,
)
}
}
val manga = if (query != null) {
if (!tags.isNullOrEmpty()) {
throw IllegalArgumentException("This source does not support search with tags")
is MangaListFilter.Advanced -> {
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
val sortOrderStr = when (filter.sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
}
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
null -> {
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
}
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
.getJSONObject("challengeSearch")
.getJSONArray("titleList")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = emptySet(),
author = jo.getStringOrNull("writingAuthorName"),
description = null,
state = null,
source = source,
)
}
} else {
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
author = jo.getStringOrNull("writingAuthorName"),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
return manga
}
@@ -309,10 +343,10 @@ internal abstract class LineWebtoonsParser(
}
fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) {
val msgpad = Calendar.getInstance().timeInMillis.toString()
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad))
val msgPad = Calendar.getInstance().timeInMillis.toString()
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgPad))
urlBuilder
.addQueryParameter("msgpad", msgpad)
.addQueryParameter("msgpad", msgPad)
.addQueryParameter("md", digest)
// .addEncodedQueryParameter("md", digest.urlEncoded())
}

View File

@@ -21,8 +21,6 @@ private const val CHAPTERS_FIRST_PAGE_SIZE = 120
private const val CHAPTERS_MAX_PAGE_SIZE = 500
private const val CHAPTERS_PARALLELISM = 3
private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug
private const val CONTENT_RATING =
"contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
private const val LOCALE_FALLBACK = "en"
@MangaSourceParser("MANGADEX", "MangaDex")
@@ -30,19 +28,17 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val configKeyDomain = ConfigKey.Domain("mangadex.org")
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.POPULARITY,
)
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported: Boolean = true
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val domain = domain
val url = buildString {
append("https://api.")
@@ -51,30 +47,65 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(PAGE_SIZE)
append("&offset=")
append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
tags?.forEach { tag ->
append("includedTags[]=")
append(tag.key)
append('&')
}
if (!query.isNullOrEmpty()) {
append("title=")
append(query.urlEncoded())
append('&')
}
append(CONTENT_RATING)
append("&order")
append(
when (sortOrder) {
SortOrder.UPDATED,
-> "[latestUploadedChapter]=desc"
append("&includes[]=cover_art&includes[]=author&includes[]=artist")
when (filter) {
is MangaListFilter.Search -> {
append("&title=")
append(filter.query)
}
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.POPULARITY -> "[followedCount]=desc"
else -> "[followedCount]=desc"
},
)
is MangaListFilter.Advanced -> {
filter.tags.forEach {
append("&includedTags[]=")
append(it.key)
}
filter.tagsExclude.forEach {
append("&excludedTags[]=")
append(it.key)
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach {
when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic")
}
}
}
append("&order")
append(
when (filter.sortOrder) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.RATING -> "[rating]=desc"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.POPULARITY -> "[followedCount]=desc"
},
)
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("ongoing")
MangaState.FINISHED -> append("completed")
MangaState.ABANDONED -> append("cancelled")
MangaState.PAUSED -> append("hiatus")
else -> append("")
}
}
filter.locale?.let {
append("&availableTranslatedLanguage[]=")
append(it.language)
}
}
null -> {
append("&order[latestUploadedChapter]=desc")
}
}
}
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
return json.mapJSON { jo ->
@@ -96,7 +127,10 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
url = id,
publicUrl = "https://$domain/title/$id",
rating = RATING_UNKNOWN,
isNsfw = attrs.getStringOrNull("contentRating") == "erotica",
isNsfw = when (attrs.getStringOrNull("contentRating")) {
"erotica", "pornographic" -> true
else -> false
},
coverUrl = cover?.plus(".256.jpg").orEmpty(),
largeCoverUrl = cover,
description = attrs.optJSONObject("description")?.selectByLocale(),
@@ -110,9 +144,11 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
source = source,
)
},
state = when (jo.getStringOrNull("status")) {
state = when (attrs.getStringOrNull("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"hiatus" -> MangaState.PAUSED
"cancelled" -> MangaState.ABANDONED
else -> null
},
author = (relations["author"] ?: relations["artist"])
@@ -164,13 +200,24 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
.getJSONArray("data")
return tags.mapJSONToSet { jo ->
MangaTag(
title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(),
title = jo.getJSONObject("attributes").getJSONObject("name")
.firstStringValue()
.toTitleCase(Locale.ENGLISH),
key = jo.getString("id"),
source = source,
)
}
}
override suspend fun getAvailableLocales(): Set<Locale> {
val head = webClient.httpGet("https://$domain/").parseHtml().head()
return head.getElementsByAttributeValue("property", "og:locale:alternate")
.mapNotNullToSet { meta ->
val raw = meta.attrOrNull("content") ?: return@mapNotNullToSet null
Locale(raw.substringBefore('_'), raw.substringAfter('_', ""))
}
}
private fun JSONObject.firstStringValue() = values().next() as String
private fun JSONObject.selectByLocale(): String? {
@@ -220,8 +267,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(limitedLimit)
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
append(offset)
append('&')
append(CONTENT_RATING)
append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
}
val json = webClient.httpGet(url).parseJson()
if (json.getString("result") == "ok") {

View File

@@ -0,0 +1,268 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArrayMap
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGAPARK", "MangaPark")
internal class MangaPark(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANGAPARK, pageSize = 36) {
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override val isTagsExclusionSupported: Boolean = true
override val configKeyDomain = ConfigKey.Domain("mangapark.net")
private val tagsMap = SuspendLazy(::parseTags)
init {
context.cookieJar.insertCookies(domain, "nsfw", "2")
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/search?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&word=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&genres=")
if (filter.tags.isNotEmpty()) {
appendAll(filter.tags, ",") { it.key }
}
append("|")
if (filter.tagsExclude.isNotEmpty()) {
appendAll(filter.tagsExclude, ",") { it.key }
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.oneOrThrowIfMany()?.let {
append(
when (it) {
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
else -> append("")
},
)
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.PAUSED -> "hiatus"
MangaState.ABANDONED -> "cancelled"
MangaState.UPCOMING -> "pending"
},
)
}
append("&sortby=")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> "views_d000"
SortOrder.UPDATED -> "field_update"
SortOrder.NEWEST -> "field_create"
SortOrder.ALPHABETICAL -> "field_name"
SortOrder.RATING -> "field_score"
else -> ""
},
)
filter.locale?.let {
append("&lang=")
append(it.language)
}
}
null -> append("&sortby=field_update")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid.gap-5 div.flex.border-b").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.src().orEmpty(),
title = div.selectFirst("h3")?.text().orEmpty(),
altTitle = null,
rating = div.selectFirst("span.text-yellow-500")?.text()?.toFloatOrNull()?.div(10F) ?: RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return tagsMap.get().values.toSet()
}
private suspend fun parseTags(): Map<String, MangaTag> {
val tagElements = webClient.httpGet("https://$domain/search").parseHtml()
.select("div.flex-col:contains(Genres) div.whitespace-nowrap")
val tagMap = ArrayMap<String, MangaTag>(tagElements.size)
for (el in tagElements) {
val name = el.selectFirstOrThrow("span.whitespace-nowrap").text().toTitleCase(sourceLocale)
if (name.isEmpty()) continue
tagMap[name] = MangaTag(
title = name,
key = el.attr("q:key") ?: continue,
source = source,
)
}
return tagMap
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"),
Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"),
Locale.CHINESE, Locale("ceb"), Locale("ca"), Locale("km"), Locale("my"),
Locale("bg"), Locale("bs"), Locale("hr"), Locale("cs"), Locale("da"),
Locale("nl"), Locale.ENGLISH, Locale("et"), Locale("fo"), Locale("fil"),
Locale("fi"), Locale("he"), Locale("ha"), Locale("jv"), Locale("lb"),
Locale("mn"), Locale("ro"), Locale("si"), Locale("ta"), Locale("uz"),
Locale("ur"), Locale("tg"), Locale("sd"), Locale("pt_br"), Locale("mo"),
Locale("lt"), Locale.JAPANESE, Locale.ITALIAN, Locale("ht"), Locale("lv"),
Locale("mr"), Locale("pt"), Locale("sn"), Locale("sv"), Locale("uk"),
Locale("tk"), Locale("sw"), Locale("st"), Locale("pl"), Locale("mi"),
Locale("lo"), Locale("ga"), Locale("gu"), Locale("gn"), Locale("id"),
Locale("ky"), Locale("mt"), Locale("fa"), Locale("sh"), Locale("es_419"),
Locale("tr"), Locale("to"), Locale("vi"), Locale("es"), Locale("sr"),
Locale("ps"), Locale("ml"), Locale("ku"), Locale("ig"), Locale("el"),
Locale.GERMAN, Locale("is"), Locale.KOREAN, Locale("ms"), Locale("ny"), Locale("sm"),
Locale("so"), Locale("ti"), Locale("zu"), Locale("yo"), Locale("th"),
Locale("sl"), Locale("ru"), Locale("no"), Locale("mg"), Locale("kk"),
Locale("hu"), Locale("ka"), Locale.FRENCH, Locale("hi"), Locale("kn"),
Locale("mk"), Locale("ne"), Locale("rm"), Locale("sk"), Locale("te"),
)
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val tagMap = tagsMap.get()
val selectTag = doc.select("div[q:key=30_2] span.whitespace-nowrap")
val tags = selectTag.mapNotNullToSet { tagMap[it.text()] }
val nsfw = tags.any { t -> t.key == "hentai" || t.key == "adult" }
val dateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
manga.copy(
altTitle = doc.selectFirst("div[q:key=tz_2]")?.text().orEmpty(),
author = doc.selectFirst("div[q:key=tz_4]")?.text().orEmpty(),
description = doc.selectFirst("react-island[q:key=0a_9]")?.html().orEmpty(),
state = when (doc.selectFirst("span[q:key=Yn_5]")?.text()?.lowercase()) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"hiatus" -> MangaState.PAUSED
"cancelled" -> MangaState.ABANDONED
else -> null
},
tags = tags,
isNsfw = nsfw,
chapters = doc.body().select("div.group.flex div.px-2").mapChapters { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val dateText = div.selectFirst("span[q:key=Ee_0]")?.text()
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
url = href,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
source = source,
scanlator = null,
branch = null,
)
},
)
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.endsWith(" ago") -> parseRelativeDate(date)
d.startsWith("just now") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> dateFormat.tryParse(date)
}
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minute", "minutes", "mins", "min").anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val script = if (doc.selectFirst("script:containsData(comic-)") != null) {
doc.selectFirstOrThrow("script:containsData(comic-)").data()
.substringAfterLast("\"comic-").split("\",\"")
} else {
doc.selectFirstOrThrow("script:containsData(manga-)").data()
.substringAfterLast("\"manga-").split("\",\"")
}
return script.mapNotNull { url ->
if (!url.startsWith("https://")) {
return@mapNotNull null
} else {
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}
}

View File

@@ -0,0 +1,321 @@
package org.koitharu.kotatsu.parsers.site.all
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.json.toJSONList
import java.util.*
internal abstract class MangaPlusParser(
context: MangaLoaderContext,
source: MangaSource,
private val sourceLang: String,
) : MangaParser(context, source), Interceptor {
private val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
)
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
// no tags or tag search available
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) {
return emptyList()
}
return when (filter) {
is MangaListFilter.Advanced -> {
when (filter.sortOrder) {
SortOrder.POPULARITY -> getPopularList()
SortOrder.UPDATED -> getLatestList()
else -> getAllTitleList()
}
}
is MangaListFilter.Search -> getAllTitleList(filter.query)
else -> getAllTitleList()
}
}
private suspend fun getPopularList(): List<Manga> {
val json = apiCall("/title_list/ranking")
return json.getJSONObject("titleRankingView")
.getJSONArray("titles")
.toJSONList()
.toMangaList()
}
private suspend fun getLatestList(): List<Manga> {
val json = apiCall("/title_list/updated")
return json.getJSONObject("titleUpdatedView")
.getJSONArray("latestTitle")
.mapJSON { it.getJSONObject("title") }
.toMangaList()
}
// since search is local, save network calls on related manga call
private val allTitleCache = SuspendLazy {
apiCall("/title_list/allV2")
.getJSONObject("allTitlesViewV2")
.getJSONArray("AllTitlesGroup")
.mapJSON { it.getJSONArray("titles").toJSONList() }
.flatten()
}
private suspend fun getAllTitleList(query: String? = null): List<Manga> {
return allTitleCache.get().toMangaList(query)
}
private fun List<JSONObject>.toMangaList(query: String? = null): List<Manga> {
return mapNotNull {
val language = it.getStringOrNull("language") ?: "ENGLISH"
if (language != sourceLang) {
return@mapNotNull null
}
val name = it.getString("name")
val author = it.getString("author")
.split('/')
.joinToString(transform = String::trim)
// filter out any other title or author which doesn't match search input
if (query != null && !(name.contains(query, true) || author.contains(query, true))) {
return@mapNotNull null
}
val titleId = it.getInt("titleId").toString()
Manga(
id = generateUid(titleId),
url = titleId,
publicUrl = "/titles/$titleId".toAbsoluteUrl(domain),
title = name,
coverUrl = it.getString("portraitImageUrl"),
altTitle = null,
author = author,
isNsfw = false,
rating = RATING_UNKNOWN,
state = null,
source = source,
tags = emptySet(),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val json = apiCall("/title_detailV3?title_id=${manga.url}")
.getJSONObject("titleDetailView")
val title = json.getJSONObject("title")
val completed = json.getJSONObject("titleLabels")
.getString("releaseSchedule").let {
it == "DISABLED" || it == "COMPLETED"
}
val hiatus = json.getStringOrNull("nonAppearanceInfo")?.contains("on a hiatus") == true
return manga.copy(
title = title.getString("name"),
publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain),
coverUrl = title.getString("portraitImageUrl"),
author = title.getString("author")
.split("/").joinToString(transform = String::trim),
description = buildString {
json.getString("overview").let(::append)
json.getStringOrNull("viewingPeriodDescription")
?.takeIf { !completed }
?.let { append("<br><br>", it) }
},
chapters = parseChapters(
json.getJSONArray("chapterListGroup"),
title.getStringOrNull("language") ?: "ENGLISH",
),
state = when {
completed -> MangaState.FINISHED
hiatus -> MangaState.PAUSED
else -> MangaState.ONGOING
},
)
}
private fun parseChapters(chapterListGroup: JSONArray, language: String): List<MangaChapter> {
val chapterList = chapterListGroup
.toJSONList()
.flatMap {
it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() +
it.optJSONArray("lastChapterList")?.toJSONList().orEmpty()
}
return chapterList.mapChapters { _, chapter ->
val chapterId = chapter.getInt("chapterId").toString()
val subtitle = chapter.getStringOrNull("subTitle") ?: return@mapChapters null
MangaChapter(
id = generateUid(chapterId),
url = chapterId,
name = subtitle,
number = chapter.getString("name")
.substringAfter("#")
.toIntOrNull() ?: -1,
uploadDate = chapter.getInt("startTimeStamp") * 1000L,
branch = when (language) {
"PORTUGUESE_BR" -> "Portuguese (Brazil)"
else -> language.lowercase().toTitleCase()
},
scanlator = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high")
.getJSONObject("mangaViewer")
.getJSONArray("pages")
return pages.mapJSONNotNull {
val mangaPage = it.optJSONObject("mangaPage")
?: return@mapJSONNotNull null
val url = mangaPage.getString("imageUrl")
val encryptionKey = mangaPage.getStringOrNull("encryptionKey")
MangaPage(
id = generateUid(url),
url = url + if (encryptionKey == null) "" else "#$encryptionKey",
preview = null,
source = source,
)
}
}
// image descrambling
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val encryptionKey = request.url.fragment
if (encryptionKey.isNullOrEmpty()) {
return response
}
val contentType = response.headers["Content-Type"] ?: "image/jpeg"
val image = requireNotNull(response.body).bytes().decodeXorCipher(encryptionKey)
val body = image.toResponseBody(contentType.toMediaTypeOrNull())
return response.newBuilder()
.body(body)
.build()
}
private fun ByteArray.decodeXorCipher(key: String): ByteArray {
val keyStream = key.chunked(2)
.map { it.toInt(16) }
return mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] }
.map(Int::toByte)
.toByteArray()
}
private suspend fun apiCall(url: String): JSONObject {
val newUrl = "$apiUrl$url".toHttpUrl().newBuilder()
.addQueryParameter("format", "json")
.build()
val response = webClient.httpGet(newUrl, extraHeaders).parseJson()
val success = response.optJSONObject("success")
return checkNotNull(success) {
val error = response.getJSONObject("error")
val reason = error.getJSONArray("popups")
.toJSONList()
.firstOrNull { it.getStringOrNull("language") == null }
if (reason?.getStringOrNull("subject") == "Not Found" && url.contains("manga_viewer")) {
"This chapter has expired"
} else {
reason?.getStringOrNull("body") ?: "Unknown Error"
}
}
}
@MangaSourceParser("MANGAPLUSPARSER_EN", "MANGA Plus English", "en")
class English(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_EN,
"ENGLISH",
)
@MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es")
class Spanish(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_ES,
"SPANISH",
)
@MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr")
class French(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_FR,
"FRENCH",
)
@MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id")
class Indonesian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_ID,
"INDONESIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
class Portuguese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_PTBR,
"PORTUGUESE_BR",
)
@MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru")
class Russian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_RU,
"RUSSIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th")
class Thai(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_TH,
"THAI",
)
@MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi")
class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_VI,
"VIETNAMESE",
)
}

View File

@@ -35,6 +35,13 @@ internal abstract class NineMangaParser(
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override val isTagsExclusionSupported: Boolean = true
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = if (request.url.host == domain) {
@@ -45,37 +52,50 @@ internal abstract class NineMangaParser(
return chain.proceed(newRequest)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/search/?name_sel=&wd=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&page=")
append(page)
append(".html")
}
!tags.isNullOrEmpty() -> {
append("/search/?category_id=")
for (tag in tags) {
append(tag.key)
append(',')
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty()) {
append("/search/?category_id=")
append(filter.tags.joinToString(separator = ",") { it.key })
append("&out_category_id=")
append(filter.tagsExclude.joinToString(separator = ",") { it.key })
filter.states.oneOrThrowIfMany()?.let {
append("&completed_series=")
when (it) {
MangaState.ONGOING -> append("no")
MangaState.FINISHED -> append("yes")
else -> append("either")
}
}
append("&page=")
} else {
append("/category/index_")
}
append("&page=")
append(page.toString())
append(".html")
}
else -> {
null -> {
append("/category/index_")
append(page)
append(".html")
}
}
append(page)
append(".html")
}
val doc = webClient.httpGet(url).parseHtml()
val root = doc.body().selectFirst("ul.direlist") ?: doc.parseFailed("Cannot find root")
@@ -171,7 +191,7 @@ internal abstract class NineMangaParser(
val cateId = el.attr("cate_id")
val a = el.selectFirstOrThrow("a")
tagMap[el.text()] = MangaTag(
title = a.text().toTitleCase(),
title = a.text().toTitleCase(sourceLocale),
key = cateId,
source = source,
)

View File

@@ -0,0 +1,387 @@
package org.koitharu.kotatsu.parsers.site.all
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import org.koitharu.kotatsu.parsers.util.json.toJSONList
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI)
internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.NINENINENINEHENTAI, size), Interceptor {
override val configKeyDomain = ConfigKey.Domain("999hentai.net")
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.NEWEST,
)
override val isMultipleTagsSupported = false
override suspend fun getAvailableLocales() = setOf(
Locale.ENGLISH,
Locale.CHINESE,
Locale.JAPANESE,
Locale("es"),
)
private fun Locale?.getSiteLang(): String {
if (this == null) return "all"
return when {
equals(Locale.ENGLISH) -> "en"
equals(Locale.CHINESE) -> "cn"
equals(Locale.JAPANESE) -> "jp"
equals(Locale("es")) -> "es"
else -> "all"
}
}
// Need for disable encoding (with encoding not working)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = if (request.header("Content-Encoding") != null) {
request.newBuilder().removeHeader("Content-Encoding").build()
} else {
request
}
return chain.proceed(newRequest)
}
private val cdnHost = SuspendLazy(::getUpdatedCdnHost)
private suspend fun getUpdatedCdnHost(): String {
val url = "https://$domain/manga-home"
val response = webClient.httpGet(url).parseHtml()
val cdn = response.selectFirst("img.v-thumbnail")?.attr("data-src")
return cdn?.toHttpUrlOrNull()?.host ?: "edge.fast4speed.rsvp"
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val query = """
queryTags(
search: {format:"tagchapter",sortBy:Popular}
page: 1
limit: 100
) {
edges {
name
}
}
""".trimIndent()
val tags = apiCall(query)
.getJSONObject("queryTags")
.getJSONArray("edges")
return tags.mapJSONToSet {
val name = it.getString("name")
MangaTag(
title = name.toCamelCase(),
key = name,
source = source,
)
}
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> {
if (filter.tags.isEmpty() && filter.sortOrder == SortOrder.POPULARITY) {
getPopularList(page, filter.locale)
} else {
getSearchList(page, null, filter.locale, filter.tags, filter.sortOrder)
}
}
is MangaListFilter.Search -> {
getSearchList(page, filter.query, null, null, filter.sortOrder)
}
else -> {
getPopularList(page, null)
}
}
}
private suspend fun getPopularList(
page: Int,
locale: Locale?,
): List<Manga> {
val query = """
queryPopularChapters(
size: $size
language: "${locale.getSiteLang()}"
dateRange: 1
page: $page
) {
edges {
_id
name
firstPics
}
}
""".trimIndent()
return apiCall(query)
.getJSONObject("queryPopularChapters")
.getJSONArray("edges")
.toMangaList()
}
private suspend fun getSearchList(
page: Int,
search: String?,
locale: Locale?,
tags: Set<MangaTag>?,
sort: SortOrder?,
): List<Manga> {
val searchPayload = buildString {
if (!search.isNullOrEmpty()) {
append("query:\"$search\",")
}
append("language:\"${locale.getSiteLang()}\"")
if (sort == SortOrder.POPULARITY) {
append(",sortBy:Popular")
}
if (!tags.isNullOrEmpty()) {
val tag = tags.oneOrThrowIfMany()!!.key
append(",tags:[\"$tag\"]")
}
}
val query = """
queryChapters(
limit: $size
search: {$searchPayload}
page: $page
) {
edges {
_id
name
firstPics
}
}
""".trimIndent()
return apiCall(query)
.getJSONObject("queryChapters")
.getJSONArray("edges")
.toMangaList()
}
private suspend fun JSONArray.toMangaList(): List<Manga> = mapJSON { entry ->
val id = entry.getString("_id")
val name = entry.getString("name")
val cover = runCatching {
entry.getJSONArray("firstPics")
.getJSONObject(0)
.getString("url")
}.getOrNull()
Manga(
id = generateUid(id),
title = name.replace(shortenTitleRegex, "").trim(),
altTitle = name,
coverUrl = when {
cover?.startsWith("http") == true -> cover
cover == null -> ""
else -> "https://${cdnHost.get()}/$cover"
},
author = null,
isNsfw = true,
url = id,
publicUrl = "/hchapter/$id".toAbsoluteUrl(domain),
tags = emptySet(),
source = source,
state = null,
rating = RATING_UNKNOWN,
)
}
override suspend fun getDetails(manga: Manga): Manga {
val query = """
queryChapter(
chapterId: "${manga.url}"
) {
_id
name
uploadDate
format
description
language
pages
tags
pictureUrls {
picCdn
pics
picsS
}
}
""".trimIndent()
val entry = apiCall(query)
.getJSONObject("queryChapter")
val id = entry.getString("_id")
val name = entry.getString("name")
val cover = entry.getJSONArray("pictureUrls")
.getJSONObject(0)
.let { pics ->
val cdn = pics.getString("picCdn").let {
if (it.startsWith("http")) {
"$it/"
} else {
"https://${cdnHost.get()}/$it/"
}
}
val img = pics.getJSONArray("pics").getJSONObject(0).getString("url")
val imgS = pics.getJSONArray("picsS").getJSONObject(0).getString("url")
Pair(cdn + imgS, cdn + img)
}
val tags = entry.optJSONArray("tags")?.mapJSON {
SiteTag(
name = it.getString("tagName"),
type = it.getStringOrNull("tagType"),
)
}
return manga.copy(
title = name.replace(shortenTitleRegex, "").trim(),
altTitle = name,
coverUrl = cover.first,
largeCoverUrl = cover.second,
author = tags?.filter { it.type == "artist" }?.joinToString { it.name.toCamelCase() },
isNsfw = true,
tags = tags?.mapToSet {
MangaTag(
title = it.name.toCamelCase(),
key = it.name,
source = source,
)
}.orEmpty(),
state = null,
description = entry.getStringOrNull("description"),
chapters = listOf(
MangaChapter(
id = generateUid(id),
name = name,
number = 1,
url = id,
uploadDate = runCatching {
dateFormat.parse(entry.getString("uploadDate"))!!.time
}.getOrDefault(0L),
branch = entry.getStringOrNull("language")?.let {
val locale = when (it) {
"en" -> Locale.ENGLISH
"jp" -> Locale.JAPANESE
"cn" -> Locale.CHINESE
"es" -> Locale("es")
else -> Locale.ROOT
}
return@let locale.getDisplayLanguage(locale)
},
scanlator = when (entry.getStringOrNull("format")) {
"artistcg" -> "ArtistCG"
"gamecg" -> "GameCG"
"imageset" -> "ImageSet"
else -> entry.getStringOrNull("format")?.toCamelCase()
},
source = source,
),
),
)
}
private data class SiteTag(
val name: String,
val type: String?,
)
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val query = """
queryRecommendations(
type: "chapter"
_id: "${seed.url}"
search: {sortBy:Popular}
page: 1
size: $size
) {
chapters {
_id
name
firstPics
}
}
""".trimIndent()
return apiCall(query)
.getJSONObject("queryRecommendations")
.getJSONArray("chapters")
.toMangaList()
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val query = """
queryChapter(
chapterId: "${chapter.url}"
) {
pictureUrls {
picCdn
pics
picsS
}
}
""".trimIndent()
val pages = apiCall(query)
.getJSONObject("queryChapter")
.getJSONArray("pictureUrls")
.getJSONObject(0)
val cdn = pages.getString("picCdn").let {
if (it.startsWith("http")) {
"$it/"
} else {
"https://${cdnHost.get()}/$it/"
}
}
val pics = pages.getJSONArray("pics").toJSONList()
val picsS = pages.getJSONArray("picsS").toJSONList()
return pics.zip(picsS).map {
val img = it.first.getString("url")
val imgS = it.second.getString("url")
MangaPage(
id = generateUid(img),
url = cdn + img,
preview = cdn + imgS,
source = source,
)
}
}
private suspend fun apiCall(query: String): JSONObject {
return webClient.graphQLQuery("https://api.$domain/api", query).getJSONObject("data")
}
companion object {
private const val size = 20
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
}

View File

@@ -11,8 +11,6 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
// see https://themewagon.com/themes/free-bootstrap-4-html5-gaming-anime-website-template-anime/
internal abstract class AnimeBootstrapParser(
context: MangaLoaderContext,
source: MangaSource,
@@ -40,13 +38,7 @@ internal abstract class AnimeBootstrapParser(
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
@@ -55,23 +47,31 @@ internal abstract class AnimeBootstrapParser(
append(page.toString())
append("&type=all")
if (!query.isNullOrEmpty()) {
append("&search=")
append(query.urlEncoded())
}
when (filter) {
is MangaListFilter.Search -> {
append("&search=")
append(filter.query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
append("&categorie=")
append(tag?.key.orEmpty())
}
is MangaListFilter.Advanced -> {
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("view")
SortOrder.UPDATED -> append("updated")
SortOrder.ALPHABETICAL -> append("default")
SortOrder.NEWEST -> append("published")
else -> append("updated")
filter.tags.oneOrThrowIfMany()?.let {
append("&categorie=")
append(it.key)
}
append("&sort=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("view")
SortOrder.UPDATED -> append("updated")
SortOrder.ALPHABETICAL -> append("default")
SortOrder.NEWEST -> append("published")
else -> append("updated")
}
}
null -> append("&sort=updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
@@ -99,7 +99,7 @@ internal abstract class AnimeBootstrapParser(
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("div.product__page__filter div:contains(Genre:) option ").mapNotNullToSet { option ->
val key = option.attr("value") ?: return@mapNotNullToSet null
val name = option.text()
val name = option.text().toTitleCase(sourceLocale)
MangaTag(
key = key,
title = name,
@@ -115,11 +115,8 @@ internal abstract class AnimeBootstrapParser(
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val state = if (doc.select(selectState).isNullOrEmpty()) {
MangaState.FINISHED
} else {

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.fr
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
@@ -13,61 +12,56 @@ import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
@MangaSourceParser("PAPSCAN", "PapScan", "fr")
internal class PapScan(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") {
override val sourceLocale: Locale = Locale.ENGLISH
override val isMultipleTagsSupported = false
override val listUrl = "/liste-manga"
override val selectState = "div.anime__details__widget li:contains(En cours)"
override val selectTag = "div.anime__details__widget li:contains(Genre) a"
override val selectChapter = "ul.chapters li"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/filterList")
append("?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&alpha=")
append(filter.query.urlEncoded())
}
if (!query.isNullOrEmpty()) {
append("&alpha=")
append(query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (!tags.isNullOrEmpty()) {
append("&cat=")
append(tag?.key.orEmpty())
}
append("&sortBy=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL -> append("name")
else -> append("updated")
filter.tags.oneOrThrowIfMany()?.let {
append("&cat=")
append(it.key)
}
append("&sortBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL_DESC -> append("name&asc=false")
SortOrder.ALPHABETICAL -> append("name&asc=true")
else -> append("updated")
}
}
null -> append("&sortBy=updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.product__item").map { div ->
val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href")
Manga(
@@ -103,17 +97,13 @@ internal class PapScan(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val state = if (doc.select(selectState).isNullOrEmpty()) {
MangaState.FINISHED
} else {
MangaState.ONGOING
}
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
MangaTag(
@@ -145,5 +135,4 @@ internal class PapScan(context: MangaLoaderContext) :
)
}
}
}

View File

@@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("KOMIKZOID", "Komikzo Id", "id")
@MangaSourceParser("KOMIKZOID", "KomikzoId", "id")
internal class KomikzoId(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz")
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.id")

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("NEUMANGA", "NeuManga", "id")
@MangaSourceParser("NEUMANGA", "NeuManga.xyz", "id")
internal class NeuManga(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz")

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

@@ -15,36 +15,81 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FLIXSCANS", "FlixScans", "ar")
@MangaSourceParser("FLIXSCANS", "FlixScans.com", "ar")
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
override val configKeyDomain = ConfigKey.Domain("flixscans.com")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val json = if (!query.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val json = when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val url = "https://api.$domain/api/v1/search/serie"
val body = JSONObject()
body.put("title", filter.query.urlEncoded())
webClient.httpPost(url, body).parseJson().getJSONArray("data")
}
val url = "https://api.$domain/api/v1/search/serie"
val body = JSONObject()
body.put("title", query.urlEncoded())
webClient.httpPost(url, body).parseJson().getJSONArray("data")
} else if (!tags.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://api.")
append(domain)
append("/api/v1/")
if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.contentRating.isNotEmpty()) {
if (page > 1) {
return emptyList()
}
append("search/advance?=")
if (filter.tags.isNotEmpty()) {
val tagQuery = filter.tags.joinToString(separator = ",") { it.key }
append("&genres=")
append(tagQuery)
}
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "droped"
MangaState.PAUSED -> "onhold"
MangaState.UPCOMING -> "soon"
},
)
}
}
filter.contentRating.oneOrThrowIfMany()?.let {
append("&adult=")
append(
when (it) {
ContentRating.ADULT -> "true"
else -> ""
},
)
}
append("&serie_type=webtoon")
} else {
append("webtoon/pages/latest/romance?page=")
append(page.toString())
}
}
webClient.httpGet(url).parseJson().getJSONArray("data")
}
null -> {
val url = "https://api.$domain/api/v1/webtoon/pages/latest/romance?page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
val tagQuery = tags.joinToString(separator = ",") { it.key }
val url = "https://api.$domain/api/v1/search/advance?=&genres=$tagQuery&serie_type=webtoon"
webClient.httpGet(url).parseJson().getJSONArray("data")
} else {
val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
@@ -62,6 +107,8 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
state = when (j.getString("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"onhold" -> MangaState.PAUSED
"droped" -> MangaState.ABANDONED
else -> null
},
author = null,
@@ -77,9 +124,9 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
return tagsList.mapNotNullToSet { idTag ->
val id = idTag.toInt()
val idKey = json.getJSONObject(id).getInt("id")
val key = json.get(idKey).toString()
val key = json.getInt(idKey).toString()
val idName = json.getJSONObject(id).getInt("name")
val name = json.get(idName).toString()
val name = json.getString(idName)
MangaTag(
key = key,
title = name,

View File

@@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("MANGASTORM", "MangaStorm", "ar")
internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("mangastorm.org")
override val isMultipleTagsSupported = false
@@ -21,35 +21,44 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url =
if (!tags.isNullOrEmpty()) {
buildString {
append("https://")
append(domain)
append("/categories/")
append(tag?.key.orEmpty())
append("?page=")
append(page)
}
} else {
buildString {
append("https://")
append(domain)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/mangas?page=")
append(page)
if (!query.isNullOrEmpty()) {
append("&query=")
append(query.urlEncoded())
append("&query=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/categories/")
append(tag?.key.orEmpty())
append("?page=")
append(page)
} else {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/mangas?page=")
append(page)
} else {
if (page > 1) {
return emptyList()
}
}
}
}
null -> {
append("/mangas?page=")
append(page)
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.row div.col").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
@@ -74,9 +83,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.selectFirstOrThrow(".card-body .col-lg-9")
return manga.copy(
altTitle = null,
state = null,

View File

@@ -17,46 +17,71 @@ import java.util.*
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val configKeyDomain = ConfigKey.Domain("team11x11.com")
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("teamxnovel.com")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/series?genre=")
append(tag?.key.orEmpty())
if (page > 1) {
append("&page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
append("/series?search=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page.toString())
}
}
} else if (!query.isNullOrEmpty()) {
append("/series?search=")
append(query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
}
} else {
when (sortOrder) {
SortOrder.POPULARITY -> append("/series")
SortOrder.UPDATED -> append("/")
else -> append("/")
}
if (page > 1) {
append("?page=")
append(page)
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/series?genre=")
append(tag?.key.orEmpty())
if (page > 1) {
append("&page=")
append(page.toString())
}
append("&")
} else {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("/series")
SortOrder.UPDATED -> append("/")
else -> append("/")
}
if (page > 1) {
append("?page=")
append(page.toString())
append("&")
} else {
append("?")
}
}
if (filter.sortOrder == SortOrder.POPULARITY || filter.tags.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append("status=")
append(
when (it) {
MangaState.ONGOING -> "مستمرة"
MangaState.FINISHED -> "مكتمل"
MangaState.ABANDONED -> "متوقف"
else -> "مستمرة"
},
)
}
}
}
null -> append("/?page=$page")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.listupd .bs .bsx").ifEmpty {
doc.select("div.post-body .box")
@@ -74,7 +99,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
tags = emptySet(),
state = when (div.selectFirst(".status")?.text()) {
"مستمرة" -> MangaState.ONGOING
"متوقف", "مكتمل" -> MangaState.FINISHED
"مكتمل" -> MangaState.FINISHED
"متوقف" -> MangaState.ABANDONED
else -> null
},
author = null,
@@ -88,7 +114,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
return doc.requireElementById("select_genre").select("option").mapNotNullToSet {
MangaTag(
key = it.attr("value"),
title = it.text(),
title = it.text().toTitleCase(sourceLocale),
source = source,
)
}
@@ -111,7 +137,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
altTitle = null,
state = when (doc.selectFirstOrThrow(".full-list-info:contains(الحالة:) a").text()) {
"مستمرة" -> MangaState.ONGOING
"متوقف", "مكتمل" -> MangaState.FINISHED
"مكتمل" -> MangaState.FINISHED
"متوقف" -> MangaState.ABANDONED
else -> null
},
tags = doc.select(".review-author-info a").mapNotNullToSet { a ->

View File

@@ -31,6 +31,7 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {

View File

@@ -15,47 +15,45 @@ internal class BeeToon(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val configKeyDomain = ConfigKey.Domain("ww7.beetoon.net")
override val configKeyDomain = ConfigKey.Domain("manhwafull.net")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("/genre/")
append(tag?.key.orEmpty())
append("/page-")
append(page)
append("/")
}
is MangaListFilter.Advanced -> {
else -> {
when (sortOrder) {
SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/")
if (filter.tags.isNotEmpty()) {
val tag = filter.tags.oneOrThrowIfMany()
append("/genre/")
append(tag?.key.orEmpty())
append("/page-")
append(page)
append("/")
} else {
when (filter.sortOrder) {
SortOrder.UPDATED -> append("/latest-update/")
SortOrder.POPULARITY -> append("/popular-manga/")
else -> append("/latest-update/")
}
append("page-")
append(page)
append("/")
}
append("page-")
append(page)
append("/")
}
null -> append("/latest-update/page-$page/")
}
}
val doc = webClient.httpGet(url).parseHtml()

View File

@@ -19,11 +19,25 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org")
@InternalParsersApi
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
if (query != null || offset > 0) {
return emptyList()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val link = when (filter) {
is MangaListFilter.Search -> {
return emptyList()
}
is MangaListFilter.Advanced -> {
if (offset > 0) {
return emptyList()
}
"https://$domain/viewer_landing.php"
}
null -> "https://$domain/viewer_landing.php"
}
val link = "https://$domain/viewer_landing.php"
val doc = webClient.httpGet(link).parseHtml()
val mangas = doc.getElementsByClass("comicPreviewContainer")
return mangas.mapNotNull { item ->

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers.site.en
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -17,6 +18,8 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("comicextra.me")
override val isMultipleTagsSupported = false
@@ -25,38 +28,62 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://$domain/")
if (!tags.isNullOrEmpty()) {
append(tag?.key.orEmpty())
if (page > 1) {
append("/")
append(page)
append("https://")
append(domain)
append("/")
when (filter) {
is MangaListFilter.Search -> {
append("comic-search?key=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page.toString())
}
}
} else if (!query.isNullOrEmpty()) {
append("comic-search?key=")
append(query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else if (filter.tags.isEmpty() && filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "/ongoing-comic"
MangaState.FINISHED -> "/completed-comic"
else -> "/ongoing-comic"
},
)
}
} else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED)
} else {
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular-comic")
SortOrder.UPDATED -> append("new-comic")
SortOrder.NEWEST -> append("recent-comic")
else -> append("new-comic")
}
}
if (page > 1) {
append("/")
append(page.toString())
}
}
} else {
when (sortOrder) {
SortOrder.POPULARITY -> append("popular-comic/")
SortOrder.UPDATED -> append("new-comic/")
SortOrder.NEWEST -> append("recent-comic/")
else -> append("new-comic/")
}
if (page > 1) {
append(page)
null -> {
append("popular-comic")
if (page > 1) {
append("/")
append(page.toString())
}
}
}
}
val doc = webClient.httpGet(url).parseHtml()

View File

@@ -1,8 +1,13 @@
package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.json.JSONArray
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -22,85 +27,128 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
append("/search?q=")
append(query.urlEncoded())
append("&")
append("classes[]".urlEncoded())
append("=Serie&page=")
append(page.toString())
} else if (!tags.isNullOrEmpty()) {
append("/tags/")
for (tag in tags) {
append(tag.key)
}
append("?view=groupings&page=")
append(page.toString())
} else {
append("/series?view=cover&page=")
append(page.toString())
}
}
val doc = webClient.httpGet(url).parseHtml()
override val isMultipleTagsSupported = false
// There are no images on the search page
if (!query.isNullOrEmpty()) {
return doc.select("dl.chapter-list dd")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("a").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
tags = div.select("span.tags a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
val url = buildString {
append("https://")
append(domain)
append("/search?q=")
append(filter.query.urlEncoded())
append("&")
append("classes[]".urlEncoded())
append("=Serie&page=")
append(page.toString())
}
} else {
return doc.select("li.span2")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("div.caption").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = setOf(),
state = null,
author = null,
source = source,
)
return parseMangaListQuery(webClient.httpGet(url).parseHtml())
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
if (filter.tags.isNotEmpty()) {
append("/tags/")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
append("?view=groupings")
} else {
append("/series?view=cover")
}
append("&page=")
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/series?view=cover&page=")
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
private fun parseMangaList(doc: Document): List<Manga> {
return doc.select("li.span2")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("div.caption").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
private fun parseMangaListQuery(doc: Document): List<Manga> {
return doc.select("dl.chapter-list dd")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("a").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
tags = div.select("span.tags a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = "https://$domain/tags?page=$page"
val root = webClient.httpGet(url).parseHtml()
return root.selectFirstOrThrow(".tag-list ").parseTags()
}
private fun Element.parseTags() = select("a").mapToSet {
MangaTag(
key = it.attr("href").removeSuffix('/').substringAfterLast('/'),
title = it.text(),
source = source,
)
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@@ -110,19 +158,14 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
altTitle = null,
state = when (root.select("h2.tag-title small").last()?.text()) {
"— Ongoing" -> MangaState.ONGOING
"— Completed" -> MangaState.FINISHED
"— Completed", "— Completed and Licensed" -> MangaState.FINISHED
"— Dropped", "— Licensed and Removed", "— Abandoned" -> MangaState.ABANDONED
"— On Hiatus" -> MangaState.PAUSED
else -> null
},
coverUrl = root.selectFirst("img.thumbnail")?.src()
.orEmpty(), // It is needed if the manga was found via the search.
tags = root.select("div.tag-tags a").mapNotNullToSet { a ->
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = href,
title = a.text(),
source = source,
)
},
tags = root.selectFirstOrThrow("div.tag-tags").parseTags(),
author = null,
description = null,
chapters = chapters,

View File

@@ -1,148 +0,0 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import java.util.*
@MangaSourceParser("FAKKU", "Fakku", "en", ContentType.HENTAI)
internal class Fakku(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FAKKU, pageSize = 25) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.NEWEST, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("fakku.cc")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/search?q=")
append(query.urlEncoded())
append("&")
}
!tags.isNullOrEmpty() -> {
append("/tags/")
append(tag?.key.orEmpty())
append("?")
}
else -> {
append("?")
}
}
append("page=")
append(page)
append("&sort=")
when (sortOrder) {
SortOrder.ALPHABETICAL -> append("title")
SortOrder.NEWEST -> append("created_at")
SortOrder.UPDATED -> append("published_at")
else -> append("published_at")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.entries .entry a").map { a ->
val href = a.attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = a.selectFirst("img")?.src().orEmpty(),
title = a.selectFirst(".title")?.text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val root = webClient.httpGet("https://$domain/tags").parseHtml()
return root.select("div.entries .entry a").mapToSet {
MangaTag(
key = it.attr("href").substringAfterLast("/"),
title = it.selectFirstOrThrow(".name").text(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val genreDeferred = async {
webClient.httpGet(manga.url.toAbsoluteUrl(domain) + ".json").parseJson()
}
val genre = genreDeferred.await()
manga.copy(
author = doc.selectFirst("tr.artists a")?.text(),
tags = if (genre.toString().contains("tags")) {
genre.getJSONArray("tags").mapJSONToSet {
MangaTag(
key = it.getString("slug"),
title = it.getString("name"),
source = source,
)
}
} else {
emptySet()
},
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
url = manga.url + "/1",
scanlator = null,
uploadDate = 0,
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val totalPages = doc.selectFirstOrThrow(".total").text().toInt()
val rawUrl = chapter.url.substringBeforeLast("/")
return (1..totalPages).map {
val url = "$rawUrl/$it"
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.selectFirstOrThrow(".page img").attrAsAbsoluteUrl("src")
}
}

View File

@@ -0,0 +1,199 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.toJSONList
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FLIXSCANSORG", "FlixScans.org", "en")
internal class FlixScansOrg(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANSORG, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
override val configKeyDomain = ConfigKey.Domain("flixscans.org")
override val isSearchSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val json = when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://api.")
append(domain)
append("/api/v1/search/advance?=&serie_type=webtoon&page=")
append(page.toString())
append("&genres=")
append(filter.tags.joinToString(separator = ",") { it.key })
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "droped"
MangaState.PAUSED -> "onhold"
MangaState.UPCOMING -> "soon"
},
)
}
filter.contentRating.oneOrThrowIfMany()?.let {
append("&adult=")
append(
when (it) {
ContentRating.ADULT -> "true"
else -> ""
},
)
}
append("&serie_type=webtoon")
}
webClient.httpGet(url).parseJson().getJSONArray("data")
}
null -> {
val url = "https://api.$domain/api/v1/search/advance?=&serie_type=webtoon&page=$page"
webClient.httpGet(url).parseJson().getJSONArray("data")
}
}
return json.mapJSON { j ->
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
val cover = "https://media.$domain/" + j.getString("thumbnail")
Manga(
id = generateUid(href),
title = j.getString("title"),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = cover,
tags = emptySet(),
state = when (j.getString("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"onhold" -> MangaState.PAUSED
"droped" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/search/advance").parseHtml()
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",")
return tagsList.mapNotNullToSet { idTag ->
val id = idTag.toInt()
val idKey = json.getJSONObject(id).getInt("id")
val key = json.getInt(idKey).toString()
val idName = json.getJSONObject(id).getInt("name")
val name = json.getString(idName)
MangaTag(
key = key,
title = name,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val chaptersDeferred = async { loadChapters(manga.url) }
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val descId = json.getJSONObject(6).getInt("story")
val desc = json.getString(descId)
val tagsId = json.getJSONObject(6).getInt("genres")
val tagsList = json.getJSONArray(tagsId).toString().replace("[", "").replace("]", "").split(",")
val ratingId = json.getJSONObject(6).getInt("rating")
val rating = json.getString(ratingId)
val nsfwId = json.getJSONObject(6).getInt("nsfw")
val nsfw = json.getBoolean(nsfwId)
manga.copy(
description = desc,
tags = tagsList.mapToSet { idTag ->
val id = idTag.toInt()
val idKey = json.getJSONObject(id).getInt("id")
val key = json.get(idKey).toString()
val idName = json.getJSONObject(id).getInt("name")
val name = json.get(idName).toString()
MangaTag(
key = key,
title = name,
source = source,
)
},
rating = rating?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = nsfw,
chapters = chaptersDeferred.await(),
)
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
private suspend fun loadChapters(baseUrl: String): List<MangaChapter> {
val key = baseUrl.substringAfter("-").substringBefore("-")
val seriesKey = baseUrl.substringAfterLast("/").substringBefore("-")
val json = JSONArray(
webClient.httpGet("https://api.$domain/api/v1/webtoon/chapters/$key-desc").parseRaw(),
).toJSONList().reversed()
return json.mapChapters { i, j ->
val url = "https://$domain/read/webtoon/$seriesKey-${j.getString("id")}-${j.getString("slug")}"
val date = j.getString("createdAt").substringBeforeLast("T")
MangaChapter(
id = generateUid(url),
url = url,
name = j.getString("slug").replace('-', ' '),
number = i + 1,
branch = null,
uploadDate = dateFormat.tryParse(date),
scanlator = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
val chapterData = json.getJSONObject(6).getInt("chapterData")
val pageLocate = json.getJSONObject(chapterData).getInt("webtoon")
val jsonPages = json.getJSONArray(pageLocate)
val pages = ArrayList<MangaPage>(jsonPages.length())
for (i in 0 until jsonPages.length()) {
val id = jsonPages.getInt(i)
val url = "https://media.$domain/" + json.getString(id)
pages.add(
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
),
)
}
return pages
}
}

View File

@@ -1,187 +0,0 @@
package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.json.JSONArray
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("KSKMOE", "Ksk.moe", "en", ContentType.HENTAI)
internal class KskMoe(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.KSKMOE, 35) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("ksk.moe")
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tags/")
append(tag?.key.orEmpty())
} else {
append("/browse")
}
if (page > 1) {
append("/page/")
append(page)
}
when (sortOrder) {
SortOrder.POPULARITY -> append("?sort=32")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("?sort=16")
SortOrder.ALPHABETICAL -> append("?sort=1")
else -> append("")
}
if (!query.isNullOrEmpty()) {
append("?s=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
if (!doc.html().contains("pagination") && page > 1) {
return emptyList()
}
return doc.requireElementById("galleries").select("article").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectLastOrThrow("h3 span").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
tags = div.select("footer span").mapNotNullToSet { span ->
MangaTag(
key = span.text().urlEncoded(),
title = span.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..2).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = if (page == 1) {
"https://$domain/tags"
} else {
"https://$domain/tags/page/$page"
}
val root = webClient.httpGet(url).parseHtml().body().getElementById("tags")
return root?.parseTags().orEmpty()
}
private fun Element.parseTags() = select("section.tags div a").mapToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
}
private val date = SimpleDateFormat("dd.MM.yyyy hh:mm 'UTC'", Locale.US)
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
return manga.copy(
tags = doc.requireElementById("metadata").select("main div:contains(Tag) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
},
author = doc.requireElementById("metadata").selectFirstOrThrow("main div:contains(Artist) a span").text(),
chapters = listOf(
MangaChapter(
id = generateUid(manga.id),
name = manga.title,
number = 1,
url = manga.url,
scanlator = null,
uploadDate = date.tryParse(doc.selectFirstOrThrow("time.updated").text()),
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url
.replace("/view/", "/read/")
.let { "$it/1" }
.toAbsoluteUrl(domain)
val document = webClient.httpGet(fullUrl).parseHtml()
val id = fullUrl
.substringAfter("/read/")
.substringBeforeLast("/")
val cdnUrl = document.selectFirst("meta[itemprop=image]")
?.attr("content")
?.toHttpUrlOrNull()
?.host
.let { "https://" + (it ?: domain) }
val script = document.select("script:containsData(window.metadata)").html()
val rawJson = script
.substringAfter("original:")
.substringBefore("resampled:")
.substringBeforeLast(",")
return JSONArray(rawJson).mapJSON {
val fileName = it.getString("n")
val url = "$cdnUrl/original/$id/$fileName"
val preview = "$cdnUrl/t/$id/320/$fileName"
MangaPage(
id = generateUid(url),
url = url,
preview = preview,
source = source,
)
}
}
}

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
@@ -17,7 +19,7 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val configKeyDomain = ConfigKey.Domain("www.mangageko.com")
override val configKeyDomain = ConfigKey.Domain("www.mgeko.com", "www.mangageko.com")
override val isMultipleTagsSupported = false
@@ -25,42 +27,49 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = if (!query.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
}
buildString {
append("https://$domain/search/?search=")
append(query.urlEncoded())
}
} else {
buildString {
append("https://$domain/browse-comics/?results=")
append(page)
append("&filter=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New")
else -> append("Updated")
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
append("/search/?search=")
append(filter.query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
append("&genre=")
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
append("/browse-comics/?results=")
append(page)
append("&filter=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("Updated")
SortOrder.NEWEST -> append("New")
else -> append("Updated")
}
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("&genre=")
append(it.key)
}
}
}
null -> {
append("/browse-comics/?results=")
append(page)
append("&filter=Updated")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("li.novel-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@@ -91,10 +100,10 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
}
}
override suspend fun getDetails(manga: Manga): Manga {
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("MMM dd, yyyy", sourceLocale)
return manga.copy(
val chaptersDeferred = async { loadChapters(manga.url) }
manga.copy(
altTitle = doc.selectFirstOrThrow(".alternative-title").text(),
state = when (doc.selectFirstOrThrow(".header-stats span:contains(Status) strong").text()) {
"Ongoing" -> MangaState.ONGOING
@@ -110,27 +119,34 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
},
author = doc.selectFirstOrThrow(".author").text(),
description = doc.selectFirstOrThrow(".description").html(),
chapters = doc.requireElementById("chapters").select("ul.chapter-list li")
.mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val url = a.attrAsRelativeUrl("href")
val name = li.selectFirstOrThrow(".chapter-title").text()
val dateText = li.select(".chapter-update").attr("datetime").substringBeforeLast(',')
.replace(".", "").replace("Sept", "Sep")
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
},
chapters = chaptersDeferred.await(),
)
}
private suspend fun loadChapters(mangaUrl: String): List<MangaChapter> {
val urlChapter = mangaUrl + "all-chapters/"
val doc = webClient.httpGet(urlChapter.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("MMM dd, yyyy", sourceLocale)
return doc.requireElementById("chapters").select("ul.chapter-list li")
.mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val url = a.attrAsRelativeUrl("href")
val name = li.selectFirstOrThrow(".chapter-title").text()
val dateText = li.select(".chapter-update").attr("datetime").substringBeforeLast(',')
.replace(".", "").replace("Sept", "Sep")
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()

View File

@@ -22,35 +22,69 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
SortOrder.UPDATED,
)
private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+")
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
)
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sortKey = when (sortOrder) {
SortOrder.ALPHABETICAL -> "?name.az"
SortOrder.RATING -> "?rating.za"
SortOrder.UPDATED -> "?last_chapter_time.za"
else -> ""
}
override val isMultipleTagsSupported = false
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val page = (offset / 30) + 1
val url = when {
!query.isNullOrEmpty() -> {
if (offset != 0) {
return emptyList()
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/search?name=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
"/search?name=${query.urlEncoded()}".toAbsoluteUrl(domain)
}
tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".toAbsoluteUrl(domain)
tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(domain)
else -> tags.joinToString(
prefix = "/search?page=$page".toAbsoluteUrl(domain),
) { tag ->
"&genres[${tag.key}]=1"
is MangaListFilter.Advanced -> {
append("/directory/")
append("0-")
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append("0")
}
append("-0-")
if (filter.states.isNotEmpty()) {
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "0"
},
)
}
} else {
append("0")
}
append("-0-0/")
append(page.toString())
append(".htm")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> ""
SortOrder.UPDATED -> "?last_chapter_time.za"
SortOrder.ALPHABETICAL -> "?name.az"
SortOrder.RATING -> "?rating.za"
else -> "?last_chapter_time.za"
},
)
}
null -> append("/directory/$page.htm?last_chapter_time.za")
}
}
val doc = webClient.httpGet(url).parseHtml()
@@ -81,7 +115,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
MangaTag(
title = x.attr("title").toTitleCase(),
key = x.attr("href").parseTagKey() ?: return@tags null,
key = x.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN,
)
}.orEmpty(),
@@ -106,7 +140,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
}?.select("a")?.mapNotNull { a ->
MangaTag(
title = a.attr("title").toTitleCase(),
key = a.attr("href").parseTagKey() ?: return@mapNotNull null,
key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN,
)
}.orEmpty(),
@@ -165,10 +199,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
?.nextElementSibling() ?: doc.parseFailed("Root not found")
return root.select("li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val key = a.attr("href").parseTagKey()
if (key.isNullOrEmpty()) {
return@mapNotNullToSet null
}
val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-")
MangaTag(
source = MangaSource.MANGATOWN,
key = key,
@@ -211,6 +242,4 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
)
}
}
private fun String.parseTagKey() = split('/').findLast { regexTag matches it }
}

View File

@@ -24,6 +24,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
SortOrder.UPDATED,
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
@@ -31,46 +32,56 @@ internal class Mangaowl(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sort = when (sortOrder) {
SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at"
SortOrder.RATING -> "rating"
else -> "modified_at"
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/8-search")
append("?q=")
append(query.urlEncoded())
when (filter) {
is MangaListFilter.Search -> {
append("/10-search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
!tags.isNullOrEmpty() -> {
append("/8-genres/")
for (tag in tags) {
is MangaListFilter.Advanced -> {
append("/10-comics")
append("?page=")
append(page.toString())
filter.tags.forEach { tag ->
append("&genres=")
append(tag.key)
}
append("?page=")
append(page.toString())
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> ""
},
)
}
append("&ordering=")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> "view_count"
SortOrder.UPDATED -> "-modified_at"
SortOrder.NEWEST -> "created_at"
SortOrder.RATING -> "rating"
else -> "modified_at"
},
)
}
else -> {
append("/8-comics")
append("?page=")
null -> {
append("/10-comics?ordering=-modified_at&page=")
append(page.toString())
append("&ordering=")
append(sort)
}
}
}
@@ -95,9 +106,9 @@ internal class Mangaowl(context: MangaLoaderContext) :
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/8-genres").parseHtml()
val doc = webClient.httpGet("https://$domain/10-genres").parseHtml()
return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a ->
val key = a.attr("href").substringAfterLast("/")
val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")
MangaTag(
key = key,
title = a.text(),
@@ -112,7 +123,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
manga.copy(
tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
key = a.attr("href").removeSuffix("/").substringAfterLast('/').substringBefore("-"),
title = a.text().toTitleCase().replace(",", ""),
source = source,
)
@@ -130,52 +141,31 @@ internal class Mangaowl(context: MangaLoaderContext) :
private fun getChapters(mangaUrl: String, doc: Document): List<MangaChapter> {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", sourceLocale)
val script = doc.selectFirstOrThrow("script:containsData(chapters:)")
val json =
script.data().substringAfter("chapters:[").substringBeforeLast(')').substringBefore("],latest_chapter:")
.split("},")
val json = script.data().substringAfter("chapters:[")
.substringBeforeLast(')')
.substringBefore("],latest_chapter:")
.split("},")
val slug = mangaUrl.substringAfterLast("/")
val chapter = ArrayList<MangaChapter>()
var lastIndexed = 0
json.mapIndexed { i, t ->
return json.mapChapters { i, t ->
if (t.contains("Chapter")) {
val id = t.substringAfter("id:").substringBefore(",created_at")
val url = "/reading/$slug/$id"
val date = t.substringAfter("created_at:\"").substringBefore("\"")
val name = t.substringAfter("name:\"").substringBefore("\"")
chapter.add(
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
uploadDate = dateFormat.tryParse(date),
source = source,
scanlator = null,
branch = null,
),
val date = t.substringAfter("created_at:\"").substringBefore('"')
val name = t.substringAfter("name:\"").substringBefore('"')
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
uploadDate = dateFormat.tryParse(date),
source = source,
scanlator = null,
branch = null,
)
lastIndexed = i
} else {
null
}
}
// last chapter
val id = script.data().substringAfter("Sign in\",").substringBefore(",\"").split(",").last()
val url = "/reading/$slug/$id"
val date = script.data().substringAfter("$id,\"").substringBefore("\",")
val name = script.data().substringAfter("$date\",\"").substringBefore("\",")
chapter.add(
MangaChapter(
id = generateUid(url),
name = name,
number = lastIndexed + 1,
url = url,
uploadDate = dateFormat.tryParse(date),
source = source,
scanlator = null,
branch = null,
),
)
return chapter
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {

View File

@@ -9,14 +9,28 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MANHWA18", "Manhwa18", "en", type = ContentType.HENTAI)
@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
class Manhwa18Parser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST)
get() = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
)
override val isTagsExclusionSupported = true
private val tagsMap = SuspendLazy(::parseTags)
@@ -29,6 +43,90 @@ class Manhwa18Parser(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&accept_genres=")
if (filter.tags.isNotEmpty()) {
append(
filter.tags.joinToString(",") { it.key },
)
}
append("&reject_genres=")
if (filter.tagsExclude.isNotEmpty()) {
append(
filter.tagsExclude.joinToString(",") { it.key },
)
}
append("&sort=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "like"
},
)
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "3"
MangaState.PAUSED -> "2"
else -> ""
},
)
}
}
null -> append("&sort=update")
}
}
val docs = webClient.httpGet(url).parseHtml()
return docs.select(".card-body .thumb-item-flow")
.map {
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
val absUrl = titleElement.attrAsAbsoluteUrl("href")
Manga(
id = generateUid(absUrl.toRelativeUrl(domain)),
title = titleElement.text(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = MangaSource.MANHWA18,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val cardInfoElement = docs.selectFirst("div.series-information")
@@ -45,6 +143,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
when (it.text().lowercase()) {
"on going" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"on hold" -> MangaState.PAUSED
else -> null
}
}
@@ -99,60 +198,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
return cal.time.time
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
else -> ""
}
val tagQuery = tags?.joinToString(",") { it.key }.orEmpty()
val url = buildString {
append("https://")
append(domain)
append("/tim-kiem?page=")
append(page)
if (!query.isNullOrEmpty()) {
append("&q=")
append(query.urlEncoded())
}
append("&accept_genres=$tagQuery")
append("&sort=")
append(sortQuery)
}
val docs = webClient.httpGet(url).parseHtml()
return docs.select(".card-body .thumb-item-flow")
.map {
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
val absUrl = titleElement.attrAsAbsoluteUrl("href")
Manga(
id = generateUid(absUrl.toRelativeUrl(domain)),
title = titleElement.text(),
altTitle = null,
url = absUrl.toRelativeUrl(domain),
publicUrl = absUrl,
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = MangaSource.MANHWA18,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(chapterUrl).parseHtml()

View File

@@ -21,29 +21,42 @@ class ManhwasMen(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.POPULARITY)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/manga-list")
append("?page=")
append(page)
when {
!query.isNullOrEmpty() -> {
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("&genero=")
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("&genero=")
append(it.key)
}
filter.states.oneOrThrowIfMany()?.let {
append("&estado=")
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "complete"
else -> ""
},
)
}
}
null -> {}
}
}
val doc = webClient.httpGet(url).parseHtml()
@@ -80,6 +93,7 @@ class ManhwasMen(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
return manga.copy(
tags = doc.body().select(".genres a").mapNotNullToSet { a ->
MangaTag(
@@ -89,9 +103,9 @@ class ManhwasMen(context: MangaLoaderContext) :
)
},
description = doc.select(".sinopsis").html(),
state = when (doc.selectLast(".anime-type-peli")?.text()?.lowercase()) {
state = when (doc.selectLast("span.anime-type-peli")?.text()?.lowercase()) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"complete" -> MangaState.FINISHED
else -> null
},
chapters = doc.select(".episodes-list li").mapChapters(reversed = true) { i, li ->
@@ -103,7 +117,7 @@ class ManhwasMen(context: MangaLoaderContext) :
url = url,
scanlator = null,
uploadDate = parseChapterDate(
SimpleDateFormat("dd/MM/yyyy", sourceLocale),
simpleDateFormat,
li.selectLastOrThrow(".flex-grow-1 span").text(),
),
branch = null,

View File

@@ -15,20 +15,28 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com")
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) {
return emptyList()
}
val url = buildString {
append("https://$domain/series")
if (!query.isNullOrEmpty()) {
append("?search=")
append(query.urlEncoded())
append("https://")
append(domain)
append("/series")
when (filter) {
is MangaListFilter.Search -> {
append("?search=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {}
null -> {}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-list").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(),

View File

@@ -24,37 +24,43 @@ internal class Pururin(context: MangaLoaderContext) :
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
append("/search?q=")
append(query.urlEncoded())
append("&page=")
append(page)
} else {
append("/browse")
if (!tags.isNullOrEmpty()) {
append("/tags/content/")
append(tag?.key.orEmpty())
append("/")
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
append("?page=")
append(page)
append("&sort=")
when (sortOrder) {
SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("most-viewed")
SortOrder.RATING -> append("highest-rated")
SortOrder.ALPHABETICAL -> append("title")
else -> append("")
is MangaListFilter.Advanced -> {
append("/browse")
filter.tags.oneOrThrowIfMany()?.let {
append("/tags/content/")
append(it.key)
append("/")
}
append("?page=")
append(page)
append("&sort=")
when (filter.sortOrder) {
SortOrder.UPDATED -> append("")
SortOrder.POPULARITY -> append("most-viewed")
SortOrder.RATING -> append("highest-rated")
SortOrder.ALPHABETICAL -> append("title")
else -> append("")
}
}
null -> {
append("/browse?page=")
append(page)
}
}
}

View File

@@ -0,0 +1,150 @@
package org.koitharu.kotatsu.parsers.site.en
import okhttp3.Headers
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
internal class ReaperComics(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.REAPERCOMICS, pageSize = 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("reaperscans.com")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) // TODO
}
is MangaListFilter.Advanced -> {
append("/")
if (filter.sortOrder == SortOrder.UPDATED) {
append("latest/")
}
append("comics?page=")
append(page.toString())
}
null -> {
append("/latest/comics?page=")
append(page.toString())
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
private fun parseMangaList(docs: Document): List<Manga> {
return docs.select("main div.relative, main li.col-span-1").map {
val a = it.selectFirstOrThrow("a")
val url = a.attrAsAbsoluteUrl("href")
Manga(
id = generateUid(url),
url = url,
title = (it.selectFirst("p a") ?: it.selectLast("a"))?.text().orEmpty(),
altTitle = null,
publicUrl = url,
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = it.selectFirstOrThrow("img").src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
return manga.copy(
description = doc.selectFirst("div.p-4 p.prose")?.html(),
state = when (doc.selectFirst("dl.mt-2 div:contains(Status) dd")?.text()?.lowercase()) {
"ongoing" -> MangaState.ONGOING
"complete" -> MangaState.FINISHED
else -> null
},
chapters = doc.select("div.p-2 div.pb-4 ul li").mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val chapterUrl = a.attrAsAbsoluteUrl("href").toRelativeUrl(domain)
MangaChapter(
id = generateUid(chapterUrl),
name = li.selectFirst("div.truncate p.truncate")?.text().orEmpty(),
number = i + 1,
url = chapterUrl,
scanlator = null,
uploadDate = parseChapterDate(
simpleDateFormat,
li.selectFirst("div.truncate div.items-center")?.text(),
),
branch = null,
source = source,
)
},
)
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.endsWith(" ago") -> parseRelativeDate(date)
else -> dateFormat.tryParse(date)
}
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("week", "weeks").anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.select("img.max-w-full").map { img ->
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.es
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -20,26 +21,35 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("templescanesp.net")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (sortOrder == SortOrder.NEWEST) {
append("/comics")
append("?page=")
append(page.toString())
} else {
if (page > 1) {
return emptyList()
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.NEWEST) {
append("/comics?page=")
append(page.toString())
} else {
if (page > 1) {
return emptyList()
}
}
}
null -> {
append("/comics?page=")
append(page.toString())
}
}
}

View File

@@ -24,49 +24,68 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.POPULARITY,
SortOrder.RATING,
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val order =
when (sortOrder) {
SortOrder.POPULARITY -> "likes_count"
SortOrder.UPDATED -> "release_date"
SortOrder.NEWEST -> "creation"
SortOrder.ALPHABETICAL -> "alphabetically"
SortOrder.RATING -> "score"
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/library")
if (query.isNullOrEmpty()) {
append("?order_item=")
append(order)
append("&order_dir=desc")
append("&filter_by=title")
if (tags != null) {
for (tag in tags) {
append("&genders[]=${tag.key}")
when (filter) {
is MangaListFilter.Search -> {
append("?title=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("?order_item=")
append(
when (filter.sortOrder) {
SortOrder.POPULARITY -> "likes_count&order_dir=desc"
SortOrder.UPDATED -> "release_date&order_dir=desc"
SortOrder.NEWEST -> "creation&order_dir=desc"
SortOrder.ALPHABETICAL -> "alphabetically&order_dir=asc"
SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc"
SortOrder.RATING -> "score&order_dir=desc"
},
)
append("&filter_by=title")
if (filter.tags.isNotEmpty()) {
for (tag in filter.tags) {
append("&genders[]=")
append(tag.key)
}
}
filter.contentRating.oneOrThrowIfMany()?.let {
append("&erotic=")
append(
when (it) {
ContentRating.SAFE -> "false"
ContentRating.ADULT -> "true"
else -> ""
},
)
}
}
} else {
append("?title=$query")
}
append("&_pg=1")
append("&page=$page")
}.toAbsoluteUrl(domain)
null -> {
append("?order_item=release_date&order_dir=desc&filter_by=title")
}
}
append("&_pg=1&page=")
append(page.toString())
}
val doc = webClient.httpGet(url, headers).parseHtml()
val items = doc.body().select("div.element")
return items.mapNotNull { item ->
@@ -95,6 +114,13 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
val contents = doc.body().selectFirstOrThrow("section.element-header-content")
return manga.copy(
description = contents.selectFirst("p.element-description")?.html(),
tags = contents.select("h6 a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringBefore("&").substringAfterLast("="),
title = a.text(),
source = source,
)
},
largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"),
state = parseStatus(contents.select("span.book-status").text().orEmpty()),
author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "),
@@ -105,9 +131,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
} else {
val chapters = ChaptersListBuilder(10)
doc.select(regularChapterListSelector).reversed().forEachIndexed { i, item ->
val chaptername = item.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim()
val scanelement = item.select("ul.chapter-list > li")
scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) }
val chapterName = item.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim()
val scanElement = item.select("ul.chapter-list > li")
scanElement.forEach { chapters.add(regularChapterFromElement(it, chapterName, i)) }
}
chapters.toList()
},

View File

@@ -25,9 +25,16 @@ internal abstract class FmreaderParser(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
)
override val isMultipleTagsSupported = false
override val availableStates: Set<MangaState> = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
)
override val isTagsExclusionSupported = true
protected open val listUrl = "/manga-list.html"
protected open val datePattern = "MMMM d, yyyy"
@@ -40,58 +47,77 @@ internal abstract class FmreaderParser(
@JvmField
protected val ongoing: Set<String> = setOf(
"On going",
"Incomplete",
"En curso",
"on going",
"incomplete",
"en curso",
)
@JvmField
protected val finished: Set<String> = setOf(
"Completed",
"Completado",
"completed",
"completado",
)
@JvmField
protected val abandoned: Set<String> = hashSetOf(
"Canceled",
"Cancelled",
"Drop",
"canceled",
"cancelled",
"drop",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("&name=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
is MangaListFilter.Advanced -> {
append("&genre=")
append(tag?.key.orEmpty())
append(filter.tags.joinToString(",") { it.key })
append("&ungenre=")
append(filter.tagsExclude.joinToString(",") { it.key })
append("&sort=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
else -> append("last_update")
}
append("&m_status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "2"
MangaState.FINISHED -> "1"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
}
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
null -> append("&sort=last_update")
}
}
val doc = webClient.httpGet(url).parseHtml()
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.thumb-item-flow").map { div ->
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
Manga(
@@ -99,8 +125,8 @@ internal abstract class FmreaderParser(
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
.substringBeforeLast("')"),
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("(")
.substringBefore(")"),
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
@@ -121,7 +147,7 @@ internal abstract class FmreaderParser(
val href = a.attr("href").substringAfter(tagPrefix).substringBeforeLast(".html")
MangaTag(
key = href,
title = a.text(),
title = a.text().toTitleCase(sourceLocale),
source = source,
)
}
@@ -140,7 +166,7 @@ internal abstract class FmreaderParser(
val desc = doc.selectFirst(selectDesc)?.html()
val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED

View File

@@ -20,79 +20,67 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
override val selectTag = "div.info-item:contains(Genre) span.info-value a"
override val datePattern = "dd/MM/yyyy"
override val selectPage = "div#chapter-content img"
override val selectBodyTag = "div.genres-menu a"
override val selectBodyTag = "div.advanced-wrapper .genre_label"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/genre/")
append(tag?.key.orEmpty())
append("?page=")
append(page.toString())
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
} else {
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
append("&q=")
append(query.urlEncoded())
}
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
append("/tim-kiem?page=")
append(page.toString())
when (filter) {
is MangaListFilter.Search -> {
append("&q=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&accept_genres=")
append(filter.tags.joinToString(",") { it.key })
append("&reject_genres=")
append(filter.tagsExclude.joinToString(",") { it.key })
append("&sort=")
append(
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "like"
},
)
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "3"
MangaState.PAUSED -> "2"
else -> ""
},
)
}
}
null -> append("&sort=update")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.thumb-item-flow").map { div ->
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
.substringBeforeLast("')"),
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectBodyTag).mapNotNullToSet { a ->
val href = a.attr("href").substringAfterLast("/")
return doc.select(selectBodyTag).mapNotNullToSet { label ->
val key = label.attr("data-genre-id")
MangaTag(
key = href,
title = a.text(),
key = key,
title = label.selectFirstOrThrow(".gerne-name").text(),
source = source,
)
}
@@ -105,7 +93,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let {
when (it.text()) {
when (it.text().lowercase()) {
in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED
else -> null

View File

@@ -14,37 +14,62 @@ internal class OlimpoScans(context: MangaLoaderContext) :
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
override val selectTag = "ul.manga-info li:contains(Género) a"
override val tagPrefix = "lista-de-comics-genero-"
override val isMultipleTagsSupported = false
override val isTagsExclusionSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append(listUrl)
append("?page=")
append(page.toString())
append("&name=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("&genre=")
append(tag?.key.orEmpty())
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/lista-de-comics-genero-")
append(it.key)
append(".html")
}
} else {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
else -> append("last_update")
}
}
append("&m_status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "2"
MangaState.FINISHED -> "1"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
}
null -> {
append(listUrl)
append("?page=")
append(page.toString())
append("&sort=last_update")
}
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
}
val doc = webClient.httpGet(url).parseHtml()

View File

@@ -3,13 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@@ -27,40 +21,7 @@ internal class Klz9(context: MangaLoaderContext) :
override val selectPage = "img"
override val selectBodyTag = "div.panel-body a"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
append("/$listUrl")
append("?page=")
append(page.toString())
when {
!query.isNullOrEmpty() -> {
append("&name=")
append(query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("&genre=")
append(tag?.key.orEmpty())
}
}
append("&sort=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name")
else -> append("last_update")
}
}
val doc = webClient.httpGet(url).parseHtml()
override fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.thumb-item-flow").map { div ->
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@@ -112,7 +73,6 @@ internal class Klz9(context: MangaLoaderContext) :
val docLoad = webClient.httpGet("https://$domain/app/manga/controllers/cont.listImg.php?cid=$cid").parseHtml()
return docLoad.select(selectPage).map { img ->
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,

View File

@@ -32,35 +32,63 @@ internal abstract class FoolSlideParser(
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) {
val url = buildString {
append("https://$domain/$searchUrl")
if (page > 1) {
return emptyList()
}
}
val q = query.urlEncoded()
webClient.httpPost(url, "search=$q").parseHtml()
} else {
val url = buildString {
append("https://$domain/$listUrl")
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
val url = buildString {
append("https://")
append(domain)
append("/")
append(searchUrl)
}
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
}
webClient.httpGet(url).parseHtml()
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/")
append(listUrl)
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
}
webClient.httpGet(url).parseHtml()
}
}
webClient.httpGet(url).parseHtml()
}
return doc.select("div.list div.group").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(

View File

@@ -1,135 +0,0 @@
package org.koitharu.kotatsu.parsers.site.foolslide.en
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("ASSORTEDSCANS", "AssortedScans", "en")
internal class AssortedScans(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.ASSORTEDSCANS, "assortedscans.com", 56) {
override val listUrl = "reader/"
override val pagination = false
override val selectInfo = "div.#series-info"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val doc = if (!query.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append('/')
append(searchUrl)
append("?q=")
append(query.urlEncoded())
}
webClient.httpGet(url).parseHtml()
} else {
val url = buildString {
append("https://$domain/$listUrl")
// For some sites that don't have enough manga and page 2 links to page 1
if (!pagination) {
if (page > 1) {
return emptyList()
}
} else {
append(page.toString())
}
}
webClient.httpGet(url).parseHtml()
}
return doc.select("section.series, tr.result").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.src().orEmpty(),// in search no img
title = div.selectFirstOrThrow("a").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val testAdultPage = webClient.httpGet(fullUrl).parseHtml()
val doc = if (testAdultPage.selectFirst("div.info form") != null) {
webClient.httpPost(fullUrl, "adult=true").parseHtml()
} else {
testAdultPage
}
val chapters = getChapters(doc)
val desc = doc.getElementById("series-desc")?.selectFirst("div")?.html()
val alt = doc.getElementById("series-aliases")?.selectFirst("div.alias")?.text()
val author = doc.getElementById("series-authors")?.selectFirst("div.author")?.text()
val state = doc.getElementById("series-status")?.selectFirst("span")?.text()
manga.copy(
coverUrl = doc.selectFirst(".cover")?.src() ?: manga.coverUrl,
description = desc,
altTitle = alt,
author = author,
state = when (state) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Canceled" -> MangaState.ABANDONED
else -> null
},
chapters = chapters,
)
}
override suspend fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select("div.chapter").mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
url = href,
uploadDate = 0,
source = source,
scanlator = null,
branch = null,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.body().select(".page-list .dropdown-list li a").map { a ->
val url = a.attr("href").toRelativeUrl(domain)
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.requireElementById("page-image").attr("src") ?: doc.parseFailed("Page image not found")
}
}

View File

@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("HNISCANTRAD", "Hni Scantrad", "fr")
@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
internal class HniScantrad(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.com") {
override val pagination = false

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("POWERMANGA", "PowerManga", "it")
internal class PowerManga(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.POWERMANGA, "reader.powermanga.org") {

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("RAMAREADER", "RamaReader", "it")
internal class Ramareader(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.RAMAREADER, "www.ramareader.it") {

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("READNIFTEAM", "ReadNifTeam", "it")
internal class ReadNifteam(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.READNIFTEAM, "read-nifteam.info") {

View File

@@ -24,39 +24,77 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.RATING,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
)
override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com")
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported: Boolean = true
init {
paginator.firstPage = 0
searchPaginator.firstPage = 0
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = urlBuilder()
.host(domain)
.addPathSegment("manga_list")
.addQueryParameter("limit", page.toString())
.addQueryParameter(
"order_by",
when (sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "views"
SortOrder.RATING -> "top"
SortOrder.NEWEST -> "create"
SortOrder.ALPHABETICAL -> "name"
},
)
if (!tags.isNullOrEmpty()) {
url.addQueryParameter("withCategories", tags.joinToString(",") { it.key })
}
if (!query.isNullOrEmpty()) {
url.addQueryParameter("search", query)
when (filter) {
is MangaListFilter.Search -> {
url.addQueryParameter("search", filter.query)
}
is MangaListFilter.Advanced -> {
when (filter.sortOrder) {
SortOrder.UPDATED -> url.addQueryParameter("order_by", "update")
.addQueryParameter("order", "desc")
SortOrder.POPULARITY -> url.addQueryParameter("order_by", "views")
.addQueryParameter("order", "desc")
SortOrder.RATING -> url.addQueryParameter("order_by", "top")
.addQueryParameter("order", "desc")
SortOrder.NEWEST -> url.addQueryParameter("order_by", "create")
.addQueryParameter("order", "desc")
SortOrder.ALPHABETICAL -> url.addQueryParameter("order_by", "name")
.addQueryParameter("order", "asc")
SortOrder.ALPHABETICAL_DESC -> url.addQueryParameter("order_by", "name")
.addQueryParameter("order", "desc")
}
if (filter.tags.isNotEmpty()) {
url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key })
}
if (filter.tagsExclude.isNotEmpty()) {
url.addQueryParameter("withoutCategories", filter.tagsExclude.joinToString(",") { it.key })
}
filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter(
"state",
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.PAUSED -> "3"
MangaState.ABANDONED -> "5"
else -> "1"
},
)
}
}
null -> url.addQueryParameter("order_by", "update")
}
val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content")
return root.select(".manga[data-manga]").map { div ->
@@ -109,7 +147,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
"En pause" -> MangaState.PAUSED
else -> null
},
author = root.selectFirst(".datas_more-authors-people")?.textOrNull().assertNotNull("author"),
author = root.selectFirst(".datas_more-authors-people")?.textOrNull(),
chapters = run {
val input = root.selectFirst("input[name=\"limit\"]") ?: return@run parseChapters(root)
val max = input.attr("max").toInt()

View File

@@ -1,148 +0,0 @@
package org.koitharu.kotatsu.parsers.site.fr
import kotlinx.coroutines.coroutineScope
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FMTEAM", "FmTeam", "fr")
internal class FmTeam(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FMTEAM, 0) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("fmteam.fr")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (page > 1) {
return emptyList()
}
val jsonManga = if (!query.isNullOrEmpty()) {
//3 letters minimum
webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson().getJSONArray("comics")
} else {
webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
}
val manga = ArrayList<Manga>(jsonManga.length())
for (i in 0 until jsonManga.length()) {
val j = jsonManga.getJSONObject(i)
val href = "/api" + j.getString("url")
when {
!tags.isNullOrEmpty() -> {
val a = j.getJSONArray("genres").toString()
var found = true
tags.forEach {
if (!a.contains(it.key, ignoreCase = true)) {
found = false
}
}
if (found) {
manga.add(
addManga(href, j),
)
}
}
else -> {
manga.add(
addManga(href, j),
)
}
}
}
return manga
}
private fun addManga(href: String, j: JSONObject): Manga {
return Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = j.getString("thumbnail"),
title = j.getString("title"),
description = j.getString("description"),
altTitle = j.getJSONArray("alt_titles").toString()
.replace("[\"", "")
.replace("\"]", "")
.replace("\",\"", " , "),
rating = j.getString("rating").toFloatOrNull()?.div(10f)
?: RATING_UNKNOWN,
tags = emptySet(),
author = j.getString("author"),
state = when (j.getString("status").lowercase()) {
"en cours" -> MangaState.ONGOING
"terminé" -> MangaState.FINISHED
else -> null
},
source = source,
isNsfw = when (j.getString("adult").toInt()) {
0 -> false
1 -> true
else -> true
},
)
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val json = webClient.httpGet(fullUrl).parseJson().getJSONObject("comic")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
val chapters = JSONArray(json.getJSONArray("chapters").toJSONList().reversed())
manga.copy(
tags = json.getJSONArray("genres").toJSONList().mapNotNullToSet {
MangaTag(
key = it.getString("slug"),
title = it.getString("name"),
source = source,
)
},
chapters = chapters.mapJSONIndexed { i, j ->
val url = "/api" + j.getString("url").toRelativeUrl(domain)
val name = j.getString("full_title")
val date = j.getStringOrNull("updated_at")
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
scanlator = null,
uploadDate = dateFormat.tryParse(date),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val jsonPages = webClient.httpGet(fullUrl).parseJson().getJSONObject("chapter").getJSONArray("pages").toString()
val pages = jsonPages.replace("[", "").replace("]", "")
.replace("\\", "").split("\",\"").drop(1)
return pages.map { url ->
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fr
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -22,6 +23,8 @@ internal class FuryoSociety(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("furyosociety.com")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
@@ -35,21 +38,27 @@ internal class FuryoSociety(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
if (page == 1) {
if (sortOrder == SortOrder.ALPHABETICAL) {
append("/mangas")
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
} else {
return emptyList()
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
append("/mangas")
}
}
null -> {}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers.site.fr
import okhttp3.Headers
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -16,9 +17,10 @@ import java.util.*
internal class LegacyScansParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.LEGACY_SCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED)
override val configKeyDomain = ConfigKey.Domain("legacy-scans.com")
@@ -26,86 +28,108 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val end = page * pageSize
val start = end - (pageSize - 1)
val url = if (!query.isNullOrEmpty()) {
if (page > 1) {
return emptyList()
}
buildString {
append("https://api.$domain/misc/home/search?title=")
append(query.urlEncoded())
}
} else {
buildString {
append("https://api.$domain/misc/comic/search/query?status=&order=&genreNames=")
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
append(tag.key)
append(',')
}
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
append("&type=&start=")
append(start)
append("&end=")
append(end)
val url = buildString {
append("https://api.$domain/misc/home/search?title=")
append(filter.query.urlEncoded())
}
return parseMangaListQuery(webClient.httpGet(url).parseJson())
}
}
val json = webClient.httpGet(url).parseJson()
return if (!query.isNullOrEmpty()) {
json.getJSONArray("results").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
tags = setOf(),
state = null,
author = null,
source = source,
)
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://api.")
append(domain)
append("/misc/comic/search/query?status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "En cours"
MangaState.FINISHED -> "Terminé"
MangaState.ABANDONED -> "Abandonné"
MangaState.PAUSED -> "En pause"
else -> ""
},
)
}
append("&order=&genreNames=")
append(filter.tags.joinToString(",") { it.key })
append("&type=&start=")
append(start)
append("&end=")
append(end)
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
} else {
json.getJSONArray("comics").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "https://api.$domain/" + j.getString("cover"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
null -> {
val url = buildString {
append("https://api.")
append(domain)
append("/misc/comic/search/query?status=&order=&genreNames=&type=&start=")
append(start)
append("&end=")
append(end)
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
}
}
private fun parseMangaList(json: JSONObject): List<Manga> {
return json.getJSONArray("comics").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "https://api.$domain/" + j.getString("cover"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
private fun parseMangaListQuery(json: JSONObject): List<Manga> {
return json.getJSONArray("results").mapJSON { j ->
val slug = j.getString("slug")
val urlManga = "https://$domain/comics/$slug"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = "",
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH)
return manga.copy(
altTitle = null,
tags = root.select("div.serieGenre span").mapNotNullToSet { span ->
@@ -156,7 +180,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
val script = doc.requireElementById("__NUXT_DATA__").data()
.substringAfterLast("\"genres\"").substringBeforeLast("\"comics\"")
.split("\",\"").drop(1)
return script.mapNotNullToSet { tag ->
MangaTag(
key = tag.substringBeforeLast("\",{"),

View File

@@ -24,36 +24,50 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val doc =
if (!query.isNullOrEmpty()) { // search only works with 4 or more letters
if (page > 1) {
return emptyList()
}
val q = query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
} else {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/manga/")
append(tag?.key.orEmpty())
}
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
append("/page/")
append(page)
append('/')
return emptyList()
}
val q = filter.query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
}
is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
filter.tags.oneOrThrowIfMany()?.let {
append("/manga/")
append(it.key)
}
if (page > 1) {
append("/page/")
append(page)
append('/')
}
}
webClient.httpGet(url).parseHtml()
}
null -> {
val url = buildString {
append("https://")
append(domain)
if (page > 1) {
append("/page/")
append(page)
append('/')
}
}
webClient.httpGet(url).parseHtml()
}
webClient.httpGet(url).parseHtml()
}
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.parsers.site.fr
import okhttp3.Headers
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -9,7 +11,6 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat
import java.util.*
@@ -21,6 +22,11 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isSearchSupported = false
override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
override val headers: Headers = Headers.Builder()
@@ -43,77 +49,103 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
throw IllegalArgumentException("Search is not supported by this source")
}
if (sortOrder == SortOrder.ALPHABETICAL) {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/api/get/catalog?page=0&filter=all")
}
val json = webClient.httpGet(url).parseJsonArray()
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = when (j.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
} else {
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
val json = webClient.httpGet(url).parseJsonArray()
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("manga_title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = null,
author = null,
source = source,
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/api/get/catalog?page=0&filter=")
filter.states.oneOrThrowIfMany()?.let {
when (it) {
MangaState.ONGOING -> append("0")
MangaState.FINISHED -> append("1")
MangaState.PAUSED -> append("4")
MangaState.ABANDONED -> append("3")
else -> append("")
}
}
}
return parseMangaListAlpha(webClient.httpGet(url).parseJsonArray())
} else {
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
null -> {
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseJsonArray())
}
}
}
private fun parseMangaList(json: JSONArray): List<Manga> {
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("manga_title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
private fun parseMangaListAlpha(json: JSONArray): List<Manga> {
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = img,
tags = setOf(),
state = when (j.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {

View File

@@ -30,46 +30,45 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
if (page == 1) {
if (!query.isNullOrEmpty()) {
when (filter) {
is MangaListFilter.Search -> {
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
append("&post_type=manga")
}
} else if (!tags.isNullOrEmpty()) {
append("/genres/")
for (tag in tags) {
append(tag.key)
}
} else {
append("/tous-nos-mangas/")
append("?order=")
when (sortOrder) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
SortOrder.ALPHABETICAL -> append("title")
SortOrder.NEWEST -> append("create")
else -> append("update")
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("/genres/")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
} else {
append("/tous-nos-mangas/?order=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("popular")
SortOrder.UPDATED -> append("update")
SortOrder.ALPHABETICAL -> append("title")
SortOrder.NEWEST -> append("create")
else -> append("update")
}
}
}
} else {
return emptyList()
}
null -> append("/tous-nos-mangas/?order=update")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.postbody .bs .bsx").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
@@ -90,7 +89,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/tous-nos-mangas/").parseHtml()
return doc.select("ul.genre li").mapNotNullToSet { li ->
@@ -104,25 +102,18 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = getChapters(doc)
val desc = doc.selectFirstOrThrow("div.desc").html()
val state = if (doc.select("div.spe span:contains(En cours)").isNullOrEmpty()) {
MangaState.FINISHED
} else {
MangaState.ONGOING
}
val alt = doc.body().select("div.infox span.alter").text()
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "")
manga.copy(
tags = doc.select("div.spe span:contains(Genres) a").mapNotNullToSet { a ->
MangaTag(
@@ -140,7 +131,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
)
}
private fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
@@ -161,10 +151,8 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val script = doc.selectFirstOrThrow("script:containsData(page_image)")
val images = JSONArray(script.data().substringAfterLast("var pages = ").substringBefore(';'))
val pages = ArrayList<MangaPage>(images.length())
for (i in 0 until images.length()) {
val pageTake = images.getJSONObject(i)
@@ -177,7 +165,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
),
)
}
return pages
}
}

View File

@@ -20,49 +20,54 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
SortOrder.UPDATED,
)
override val isMultipleTagsSupported = false
override val configKeyDomain = ConfigKey.Domain("scantrad-union.com")
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page.toString())
append("/?s=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("/tag/")
for (tag in tags) {
append(tag.key)
append(',')
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
append("/page/")
append(page.toString())
append("/")
}
} else {
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
append("/manga/page/")
append(page.toString())
append("/")
}
if (filter.sortOrder == SortOrder.UPDATED && page > 1) {
return emptyList()
}
}
append("/page/")
}
null -> {
append("/manga/page/")
append(page.toString())
}
else -> {
if (sortOrder == SortOrder.ALPHABETICAL) {
append("/manga/")
append("/page/")
append(page.toString())
}
if (sortOrder == SortOrder.UPDATED) {
append("")
}
append("/")
}
}
}
@@ -177,12 +182,10 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
val body = doc.body()
val root = body.select(".asp_gochosen")[1]
val list = root?.select("option").orEmpty()
val list = body.select(".asp_gochosen")[1].select("option").orEmpty()
return list.mapToSet { li ->
MangaTag(
key = li.text(),
key = li.text().lowercase().replace(" ", "-"),
title = li.text(),
source = source,
)

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@@ -24,36 +25,42 @@ internal abstract class GalleryAdultsParser(
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/?")
} else {
append("/tag/")
append(tag?.key.orEmpty())
append("/?")
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=")
append(filter.query.urlEncoded())
append("&")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
is MangaListFilter.Advanced -> {
val tag = filter.tags.oneOrThrowIfMany()
val lang = filter.locale
if (tag != null && lang != null) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
}
if (tag != null) {
append("/tag/")
append(tag.key)
append("/?")
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/?")
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
@@ -96,39 +103,30 @@ internal abstract class GalleryAdultsParser(
}.awaitAll().flattenTo(ArraySet(360))
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("tr"),
Locale("th"),
Locale("vi"),
)
protected open val pathTagUrl = "/tags/popular/?page="
protected open val selectTags = ".tags_page ul.tags li"
protected open val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/chinese",
"/spanish",
"/russian",
"/korean",
"/german",
"/indonesian",
"/italian",
"/portuguese",
"/turkish",
"/thai",
"/vietnamese",
) // The "/" is used to move them up in the tag list and therefore also in the url.
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = "https://$domain$pathTagUrl$page"
val root = webClient.httpGet(url).parseHtml().selectFirstOrThrow(selectTags)
val tagLanguage = ArrayList<MangaTag>(listLanguage.size)
for (language in listLanguage) {
tagLanguage.add(
MangaTag(
key = "languageKey",
title = language,
source = source,
),
)
}
return root.parseTags() + tagLanguage
return root.parseTags()
}
protected open fun Element.parseTags() = select("a").mapToSet {
@@ -136,7 +134,7 @@ internal abstract class GalleryAdultsParser(
val name = it.html().substringBefore("<")
MangaTag(
key = key,
title = name,
title = name.toTitleCase(),
source = source,
)
}
@@ -150,6 +148,9 @@ internal abstract class GalleryAdultsParser(
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val urlChapters = doc.selectFirstOrThrow(selectUrlChapter).attr("href")
val tag = doc.selectFirst(selectTag)?.parseTags()
val branch = doc.select(selectLanguageChapter).joinToString(separator = " / ") {
it.html().substringBefore("<")
}
return manga.copy(
tags = tag.orEmpty(),
author = doc.selectFirst(selectAuthor)?.html()?.substringBefore("<span"),
@@ -161,7 +162,7 @@ internal abstract class GalleryAdultsParser(
url = urlChapters,
scanlator = null,
uploadDate = 0,
branch = doc.selectFirst(selectLanguageChapter)?.html()?.substringBefore("<"),
branch = branch,
source = source,
),
),
@@ -195,4 +196,8 @@ internal abstract class GalleryAdultsParser(
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.requireElementById(idImg).src() ?: doc.parseFailed("Image src not found")
}
protected open fun Locale.toLanguagePath() = when (language) {
else -> getDisplayLanguage(Locale.ENGLISH).lowercase()
}
}

View File

@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.removeSuffix
import java.util.*
@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI)
internal class AsmHentai(context: MangaLoaderContext) :
@@ -17,11 +20,12 @@ internal class AsmHentai(context: MangaLoaderContext) :
override val pathTagUrl = "/tags/?page="
override val selectAuthor = "div.tags:contains(Artists:) .tag_list a span.tag"
override val idImg = "fimg"
override val listLanguage = arrayOf(
"/english",
"/japanese",
"/chinese",
"/turkish",
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("tr"),
)
override fun Element.parseTags() = select("a").mapToSet {

View File

@@ -6,10 +6,11 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("DOUJINDESUUK", "DoujinDesu.uk", type = ContentType.HENTAI)
internal class DoujinDesuUk(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.DOUJINDESUUK, "doujindesu.uk", 50) {
GalleryAdultsParser(context, MangaSource.DOUJINDESUUK, "doujindesu.uk", 25) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"
@@ -19,10 +20,11 @@ internal class DoujinDesuUk(context: MangaLoaderContext) :
override val selectAuthor = "div.tag-container:contains(Artists) a"
override val selectLanguageChapter = "div.tag-container:contains(Languages) a"
override val idImg = "image-container"
override val listLanguage = arrayOf(
"/english",
"/japanese",
"/chinese",
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
)
override fun parseMangaList(doc: Document): List<Manga> {

View File

@@ -1,65 +1,92 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.internal.StringUtil
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("HENTAI3", "3Hentai", type = ContentType.HENTAI)
internal class Hentai3(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAI3, "3hentai.net") {
GalleryAdultsParser(context, MangaSource.HENTAI3, "3hentai.net", pageSize = 30) {
override val selectGallery = ".doujin "
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".title"
override val pathTagUrl = "/tags-popular/"
override val selectTags = "span.filter-elem"
override val selectTag = "div.tag-container:contains(Tags :) .filter-elem"
override val selectAuthor = "div.tag-container:contains(Artistes :) .filter-elem"
override val selectLanguageChapter = "div.tag-container:contains(Langues :) .filter-elem"
override val selectTags = "div.tag-listing-container"
override val selectTag = "div.tag-container:contains(Tags)"
override val selectAuthor = "div.tag-container:contains(Artists) .filter-elem a"
override val selectLanguageChapter = "div.tag-container:contains(Languages) a"
override val selectUrlChapter = "#main-cover a"
override val idImg = ".js-main-img"
override val listLanguage = arrayOf(
"/english",
"/spanish",
"/french",
"/italian",
"/portuguese",
"/russian",
"/japanese",
override val isMultipleTagsSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale.ITALIAN,
Locale("pt"),
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/")
append(page)
} else {
append("/tags/")
append(tag?.key.orEmpty())
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
append(page.toString())
}
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=")
append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
append(page.toString())
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) {
append("?sort=popular")
}
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tags/")
append(it.key)
}
append("/")
append(page.toString())
if (filter.sortOrder == SortOrder.POPULARITY) {
append("?sort=popular")
}
} else {
append("/")
append(page)
}
}
null -> {
append("/")
append(page)
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&page=")
append(page)
} else {
append("/")
append(page)
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
@@ -69,4 +96,19 @@ internal class Hentai3(context: MangaLoaderContext) :
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
}
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
val joiner = StringUtil.StringJoiner(" ")
tags.forEach { tag ->
joiner.add("tag:\"")
joiner.append(tag.key)
joiner.append("\"")
}
language?.let { lc ->
joiner.add("language:\"")
joiner.append(lc.toLanguagePath())
joiner.append("\"")
}
return joiner.complete()
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
@@ -8,10 +9,11 @@ import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.*
@MangaSourceParser("HENTAIENVY", "HentaiEnvy", type = ContentType.HENTAI)
internal class HentaiEnvy(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIENVY, "hentaienvy.com") {
GalleryAdultsParser(context, MangaSource.HENTAIENVY, "hentaienvy.com", pageSize = 24) {
override val selectGalleryLink = "a"
override val selectGalleryTitle = "div.title"
override val selectTags = ".tags_items"
@@ -19,48 +21,60 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
override val selectAuthor = ".gt_right_tags ul:contains(Artists:) a"
override val selectLanguageChapter = ".gt_right_tags ul:contains(Languages:) a"
override val idImg = "fimg"
override val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/chinese",
"/spanish",
"/russian",
"/korean",
"/german",
"/portuguese",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/?")
} else {
append("/tag/")
append(tag?.key.orEmpty())
append("/?")
when (filter) {
is MangaListFilter.Search -> {
append("/search/?s_key=")
append(filter.query.urlEncoded())
append("&")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?s_key=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
if (filter.locale != null) {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
}
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/popular")
}
append("/?")
}
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/?")
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("pt"),
)
}

View File

@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("HENTAIERA", "HentaiEra", type = ContentType.HENTAI)
internal class HentaiEra(context: MangaLoaderContext) :
@@ -14,14 +15,20 @@ internal class HentaiEra(context: MangaLoaderContext) :
override val selectTag = ".galleries_info li:contains(Tags) div.info_tags"
override val selectAuthor = ".galleries_info li:contains(Artists) span.item_name"
override val selectLanguageChapter = ".galleries_info li:contains(Languages) div.info_tags .item_name"
override val listLanguage = arrayOf(
"/english",
"/japanese",
"/spanish",
"/french",
"/korean",
"/german",
"/russian",
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
)
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
@@ -34,43 +41,89 @@ internal class HentaiEra(context: MangaLoaderContext) :
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/?")
} else {
append("/tag/")
append(tag?.key.orEmpty())
append("/?")
when (filter) {
is MangaListFilter.Search -> {
append("/search/?key=")
append(filter.query.urlEncoded())
append("&")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?key=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?key=")
if (filter.sortOrder == SortOrder.POPULARITY) {
append(
buildQuery(filter.tags, filter.locale)
.replace("&lt=1&dl=0&pp=0&tr=0", "&lt=0&dl=0&pp=1&tr=0"),
)
} else {
append(buildQuery(filter.tags, filter.locale))
}
append("&")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else {
append("/?")
}
}
null -> append("/?")
}
append("page=")
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
private fun buildQuery(tags: Collection<MangaTag>, locale: Locale?): String {
val queryDefault =
"&search=&mg=1&dj=1&ws=1&is=1&ac=1&gc=1&en=0&jp=0&es=0&fr=0&kr=0&de=0&ru=0&lt=1&dl=0&pp=0&tr=0"
val tag = tags.joinToString(" ", postfix = " ") { it.key }
val queryMod = when (val lp = locale?.toLanguagePath()) {
"english" -> queryDefault.replace("en=0", "en=1")
"japanese" -> queryDefault.replace("jp=0", "jp=1")
"spanish" -> queryDefault.replace("es=0", "es=1")
"french" -> queryDefault.replace("fr=0", "fr=1")
"korean" -> queryDefault.replace("kr=0", "kr=1")
"russian" -> queryDefault.replace("ru=0", "ru=1")
"german" -> queryDefault.replace("de=0", "de=1")
null -> "&search=&mg=1&dj=1&ws=1&is=1&ac=1&gc=1&en=1&jp=1&es=1&fr=1&kr=1&de=1&ru=1&lt=1&dl=0&pp=0&tr=0"
else -> throw IllegalArgumentException("Unsupported locale: $lp")
}
return tag + queryMod
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val urlChapters = doc.selectFirstOrThrow("#cover a, .cover a, .left_cover a").attr("href")
val tag = doc.selectFirst(selectTag)?.parseTags()
val branch = doc.select(selectLanguageChapter).joinToString(separator = " / ") {
it.text()
}
return manga.copy(
tags = tag.orEmpty(),
author = doc.selectFirst(selectAuthor)?.text(),
@@ -82,7 +135,7 @@ internal class HentaiEra(context: MangaLoaderContext) :
url = urlChapters,
scanlator = null,
uploadDate = 0,
branch = doc.selectFirst(selectLanguageChapter)?.text(),
branch = branch,
source = source,
),
),

View File

@@ -1,14 +1,16 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.internal.StringUtil
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("HENTAIFORCE", "HentaiForce", type = ContentType.HENTAI)
internal class HentaiForce(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIFORCE, "hentaiforce.net") {
GalleryAdultsParser(context, MangaSource.HENTAIFORCE, "hentaiforce.net", pageSize = 30) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a.gallery-thumb"
override val pathTagUrl = "/tags/popular/"
@@ -18,20 +20,25 @@ internal class HentaiForce(context: MangaLoaderContext) :
override val selectAuthor = "div.tag-container:contains(Artists:) a"
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
override val idImg = ".gallery-reader-img-wrapper img"
override val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/chinese",
"/spanish",
"/russian",
"/korean",
"/german",
"/indonesian",
"/italian",
"/portuguese",
"/thai",
"/vietnamese",
override val isMultipleTagsSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("id"),
Locale.ITALIAN,
Locale("pt"),
Locale("th"),
Locale("vi"),
)
override suspend fun getPageUrl(page: MangaPage): String {
@@ -39,35 +46,70 @@ internal class HentaiForce(context: MangaLoaderContext) :
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/")
} else {
append("/tag/")
append(tag?.key.orEmpty())
append("/")
when (filter) {
is MangaListFilter.Search -> {
append("/search?q=")
append(filter.query.urlEncoded())
append("&page=")
}
} else if (!query.isNullOrEmpty()) {
append("search?q=")
append(query.urlEncoded())
append("&page=")
} else {
append("/page/")
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search?q=")
append(buildQuery(filter.tags, filter.locale))
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&page=")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else {
append("/page/")
}
}
null -> append("/page/")
}
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
val joiner = StringUtil.StringJoiner(" ")
tags.forEach { tag ->
joiner.add("tag:\"")
joiner.append(tag.key)
joiner.append("\"")
}
language?.let { lc ->
joiner.add("language:\"")
joiner.append(lc.toLanguagePath())
joiner.append("\"")
}
return joiner.complete()
}
}

View File

@@ -1,10 +1,13 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI)
internal class HentaiFox(context: MangaLoaderContext) :
@@ -14,65 +17,113 @@ internal class HentaiFox(context: MangaLoaderContext) :
override val selectTags = ".list_tags"
override val selectTag = "ul.tags"
override val selectLanguageChapter = "ul.languages a.tag_btn"
override val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/chinese",
"/spanish",
"/russian",
"/korean",
"/indonesian",
"/italian",
"/portuguese",
"/turkish",
"/thai",
"/vietnamese",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val isMultipleTagsSupported = true
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
} else {
append("/tag/")
append(tag?.key.orEmpty())
}
if (page > 1) {
append("/pag/")
append(page)
append("/")
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page.toString())
}
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?q=")
append(buildQuery(filter.tags, filter.locale))
if (page > 1) {
append("&page=")
append(page.toString())
}
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
if (page > 1) {
append("/pag/")
append(page.toString())
append("/")
}
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
if (page > 1) {
append("/pag/")
append(page.toString())
append("/")
}
} else {
if (page > 2) {
append("/pag/")
append(page.toString())
append("/")
} else if (page > 1) {
append("/page/")
append(page.toString())
append("/")
}
}
}
} else {
if (page > 2) {
append("/pag/")
append(page)
append("/")
} else if (page > 1) {
append("/page/")
append(page)
append("/")
null -> {
if (page > 2) {
append("/pag/")
append(page.toString())
append("/")
} else if (page > 1) {
append("/page/")
append(page.toString())
append("/")
}
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override fun Element.parseTags() = select("a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
val name = it.selectFirst(".list_tag")?.text() ?: it.html().substringBefore("<")
MangaTag(
key = key,
title = name,
source = source,
)
}
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
val joiner = StringUtil.StringJoiner(" ")
tags.forEach { tag ->
joiner.add(tag.key)
}
language?.let { lc ->
joiner.add(lc.toLanguagePath())
}
return joiner.complete()
}
}

View File

@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.removeSuffix
import java.util.*
@MangaSourceParser("HENTAIROX", "HentaiRox", type = ContentType.HENTAI)
internal class HentaiRox(context: MangaLoaderContext) :
@@ -16,14 +19,15 @@ internal class HentaiRox(context: MangaLoaderContext) :
override val selectTag = "li:contains(Tags:)"
override val selectAuthor = "li:contains(Artists:) span.item_name"
override val selectLanguageChapter = "li:contains(Languages:) .item_name"
override val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/spanish",
"/russian",
"/korean",
"/german",
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
)
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {

View File

@@ -1,11 +1,13 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("NHENTAI", "NHentai.net", type = ContentType.HENTAI)
internal class NHentaiParser(context: MangaLoaderContext) :
@@ -14,50 +16,71 @@ internal class NHentaiParser(context: MangaLoaderContext) :
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"
override val pathTagUrl = "/tags/popular?page="
override val selectTags = "#tag-container a"
override val selectTags = "#tag-container"
override val selectTag = ".tag-container:contains(Tags:) span.tags"
override val selectAuthor = "#tags div.tag-container:contains(Artists:) span.name"
override val selectLanguageChapter =
".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated
override val idImg = "image-container"
override val listLanguage = arrayOf(
"/english",
"/japanese",
"/chinese",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val isMultipleTagsSupported = true
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
if (tag.key == "languageKey") {
append("/language")
append(tag.title)
append("/?")
} else {
append("/tag/")
append(tag.key)
append("/?")
when (filter) {
is MangaListFilter.Search -> {
append("/search/?q=pages:>0 ")
append(filter.query.urlEncoded())
append("&")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
is MangaListFilter.Advanced -> {
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
append("/search/?q=")
append(buildQuery(filter.tags, filter.locale).urlEncoded())
if (filter.sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
append("&")
} else if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else if (filter.locale != null) {
append("/language/")
append(filter.locale.toLanguagePath())
append("/")
if (filter.sortOrder == SortOrder.POPULARITY) {
append("popular/")
}
append("?")
} else {
if (filter.sortOrder == SortOrder.POPULARITY) {
append("/?sort=popular&")
} else {
append("/?")
}
}
}
null -> append("/?")
}
append("page=")
append(page)
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
@@ -78,7 +101,24 @@ internal class NHentaiParser(context: MangaLoaderContext) :
)
}
private fun buildQuery(tags: Collection<MangaTag>) = tags.joinToString(separator = " ") { tag ->
"tag:\"${tag.key}\""
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale.CHINESE,
)
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
val joiner = StringUtil.StringJoiner(" ")
tags.forEach { tag ->
joiner.add("tag:\"")
joiner.append(tag.key)
joiner.append("\"")
}
language?.let { lc ->
joiner.add("language:\"")
joiner.append(lc.toLanguagePath())
joiner.append("\"")
}
return joiner.complete()
}
}

View File

@@ -1,71 +0,0 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.lang.IllegalArgumentException
@MangaSourceParser("NHENTAIUK", "NHentai.uk", type = ContentType.HENTAI)
internal class NHentaiUk(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.NHENTAIUK, "nhentai.uk", 50) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"
override val pathTagUrl = "/tags/popular?p="
override val selectTags = "#tag-container"
override val selectTag = "div.tag-container:contains(Tags:) span.tags"
override val selectAuthor = "div.tag-container:contains(Artists:) a"
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
override val idImg = "image-container"
override val listLanguage = arrayOf(
"/english",
"/french",
"/japanese",
"/chinese",
"/spanish",
"/russian",
"/korean",
"/german",
"/italian",
"/portuguese",
"/turkish",
)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
if (tag?.key == "languageKey") {
append("/language")
append(tag.title)
append("/?p=")
} else {
append("/tag/")
append(tag?.key)
append("/?p=")
}
} else if (!query.isNullOrEmpty()) {
throw IllegalArgumentException("Search is not supported by this source")
} else {
append("/home?p=")
}
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.requireElementById(idImg).selectFirstOrThrow("img").src() ?: root.parseFailed("Image src not found")
}
}

View File

@@ -0,0 +1,150 @@
package org.koitharu.kotatsu.parsers.site.gattsu
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
internal abstract class GattsuParser(
context: MangaLoaderContext,
source: MangaSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
protected open val tagPrefix = "tag"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/page/")
append(page.toString())
append("/?s=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix/")
append(it.key)
}
append("/page/")
append(page.toString())
}
null -> {
append("/page/")
append(page.toString())
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsAbsoluteUrl("href")
if (!href.contains(domain)) {
//Some sources include ads in manga lists
return@mapNotNull null
}
Manga(
id = generateUid(href),
url = href,
publicUrl = href,
title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(),
coverUrl = li.selectFirst("img")?.src().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
description = null,
state = null,
author = null,
isNsfw = isNsfwSource,
source = source,
)
}
}
protected open val tagUrl = "generos"
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml()
return doc.selectLastOrThrow(".meio-conteudo p, div.lista-tags ul").parseTags()
}
protected open fun Element.parseTags() = select("a").mapToSet {
val key = it.attr("href").removeSuffix("/").substringAfterLast("/")
val name = it.selectFirst(".tag-titulo")?.text() ?: key
MangaTag(
key = key,
title = name,
source = source,
)
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val urlChapter = doc.selectFirstOrThrow("ul.post-fotos li a, ul.paginaPostBotoes a").attrAsAbsoluteUrl("href")
return manga.copy(
description = doc.selectFirst("div.post-texto")?.html(),
tags = doc.selectFirst(".post-itens li:contains(Tags), .paginaPostInfo li:contains(Categorias)")
?.parseTags().orEmpty(),
author = doc.selectFirst(".post-itens li:contains(Autor) a, .paginaPostInfo li:contains(Artista) a")
?.text(),
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
url = urlChapter,
scanlator = null,
uploadDate = 0,
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val totalPages =
doc.selectLastOrThrow("div.galeria-paginacao span").text().substringAfterLast("- ").substringBeforeLast(')')
.toInt()
val rawUrl = chapter.url.substringBeforeLast("=")
return (1..totalPages).map {
val url = "$rawUrl=$it"
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.selectFirstOrThrow("div.galeria-foto img").src() ?: doc.parseFailed("Image src not found")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
@MangaSourceParser("HENTAISEASON", "HentaiSeason", "pt", ContentType.HENTAI)
internal class HentaiSeason(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.HENTAISEASON, "hentaiseason.com")

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
@MangaSourceParser("HENTAITOKYO", "HentaiTokyo", "pt", ContentType.HENTAI)
internal class HentaiTokyo(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.HENTAITOKYO, "hentaitokyo.net") {
override val tagUrl = "tags"
}

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.selectLastOrThrow
import org.koitharu.kotatsu.parsers.util.src
@MangaSourceParser("MUNDOHENTAIOFICIAL", "MundoHentaiOficial", "pt", ContentType.HENTAI)
internal class MundoHentaiOficial(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.MUNDOHENTAIOFICIAL, "mundohentaioficial.com") {
override val tagUrl = "tags"
override fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li ->
val a = li.selectLastOrThrow("a")
val href = a.attrAsAbsoluteUrl("href")
if (!href.contains(domain)) {
//Some sources include ads in manga lists
return@mapNotNull null
}
Manga(
id = generateUid(href),
url = href,
publicUrl = href,
title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(),
coverUrl = li.selectFirst("img")?.src().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
description = null,
state = null,
author = null,
isNsfw = isNsfwSource,
source = source,
)
}
}
}

View File

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("UNIVERSOHENTAI", "UniversoHentai", "pt", ContentType.HENTAI)
internal class UniversoHentai(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.UNIVERSOHENTAI, "universohentai.com") {
override val tagPrefix = "category"
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/tags/").parseHtml()
return doc.requireElementById("menu-topo").parseTags()
}
override fun Element.parseTags() = select("a").mapNotNullToSet {
if (!it.attr("href").contains("/category/")) return@mapNotNullToSet null
val key = it.attr("href").removeSuffix("/").substringAfterLast("/")
MangaTag(
key = key,
title = it.text(),
source = source,
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val images = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml().requireElementById("galeria")
.select(".galeria-foto img")
return images.map { img ->
val urlImages = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(urlImages),
url = urlImages,
preview = null,
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
}

View File

@@ -0,0 +1,131 @@
package org.koitharu.kotatsu.parsers.site.guya
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
internal abstract class GuyaParser(
context: MangaLoaderContext,
source: MangaSource,
domain: String,
pageSize: Int = 0,
) : PagedMangaParser(context, source, pageSize) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain(domain)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) return emptyList()
val url = buildString {
append("https://")
append(domain)
append("/api/get_all_series/")
}
when (filter) {
is MangaListFilter.Search -> {
return parseMangaList(webClient.httpGet(url).parseJson(), filter.query)
}
is MangaListFilter.Advanced -> {}
null -> {}
}
return parseMangaList(webClient.httpGet(url).parseJson(), "")
}
protected open fun parseMangaList(json: JSONObject, query: String): List<Manga> {
val manga = ArrayList<Manga>(json.length())
val keys: Iterator<String> = json.keys()
while (keys.hasNext()) {
val key = keys.next()
if (json.get(key) is JSONObject) {
if (query.isNotEmpty()) {
if (key.lowercase().contains(query.lowercase())) manga.add(addManga(json.getJSONObject(key), key))
} else manga.add(addManga(json.getJSONObject(key), key))
}
}
return manga
}
private fun addManga(j: JSONObject, name: String): Manga {
val url = "https://$domain/read/manga/" + j.getString("slug")
val apiUrl = "https://$domain/api/series/" + j.getString("slug")
return Manga(
id = generateUid(apiUrl),
url = apiUrl,
publicUrl = url,
title = name,
coverUrl = j.getString("cover").toAbsoluteUrl(domain),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
description = j.getString("description"),
state = null,
author = j.getString("author"),
isNsfw = isNsfwSource,
source = source,
)
}
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val json = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseJson().getJSONObject("chapters")
val slug = manga.url.removeSuffix('/').substringAfterLast('/')
val keys: Iterator<String> = json.keys()
val chapters = ArrayList<MangaChapter>()
var i = 0
while (keys.hasNext()) {
val key = keys.next()
++i
val chapter = json.getJSONObject(key)
val url = "https://$domain/api/series/$slug/$key"
chapters.add(
MangaChapter(
id = generateUid(url),
name = chapter.getString("title"),
number = i,
url = url,
scanlator = null,
uploadDate = 0,
branch = null,
source = source,
),
)
}
return manga.copy(chapters = chapters)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val key = chapter.url.substringAfterLast('/')
val url = chapter.url.substringBeforeLast('/')
val slug = url.substringAfterLast('/')
val chapterPages = webClient.httpGet(url.toAbsoluteUrl(domain)).parseJson()
.getJSONObject("chapters").getJSONObject(key)
val images = chapterPages.getJSONObject("groups")
val folder = chapterPages.getString("folder")
val keysPages: Iterator<String> = images.keys()
val firstKey = keysPages.next()
val jsonPages = images.getJSONArray(firstKey)
val pages = ArrayList<MangaPage>(jsonPages.length())
for (i in 0 until jsonPages.length()) {
val urlPage = "https://$domain/media/manga/$slug/chapters/$folder/$firstKey/" + jsonPages.getString(i)
pages.add(
MangaPage(
id = generateUid(urlPage),
url = urlPage,
preview = null,
source = source,
),
)
}
return pages
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.guya.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("MAHOUSHOUJOBU", "MahouShoujobu")
internal class MahouShoujobu(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.MAHOUSHOUJOBU, "mahoushoujobu.com")

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.guya.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("DANKE", "DankeFursLesen", "en")
internal class Danke(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.DANKE, "danke.moe")

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.guya.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("GUYACUBARI", "GuyaCubari", "en")
internal class GuyaCubari(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.GUYACUBARI, "guya.cubari.moe")

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.parsers.site.guya.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("HACHIRUMI", "Hachirumi", "en", ContentType.HENTAI)
internal class Hachirumi(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.HACHIRUMI, "hachirumi.com")

View File

@@ -27,6 +27,9 @@ internal abstract class HeanCms(
SortOrder.POPULARITY,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
@@ -34,50 +37,55 @@ internal abstract class HeanCms(
protected open val pathManga = "series"
//For some sources, you need to send a json. For the moment, this part only works in Get. ( ex source need json gloriousscan.com , omegascans.org )
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domain)
append("/query?query_string=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
}
append("&series_status=All&order=desc&orderBy=")
when (sortOrder) {
SortOrder.POPULARITY -> append("total_views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("created_at")
SortOrder.ALPHABETICAL -> append("title")
else -> append("latest")
}
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&tags_ids=")
append("[".urlEncoded())
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
// Just to make it fit [1,2,44] ect
if (!firstTag) {
firstTag = true
} else {
append(",")
}
append(tag.key)
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=desc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
else -> append("latest&order=desc")
}
append("&series_type=Comic&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("]".urlEncoded())
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
@@ -90,7 +98,7 @@ internal abstract class HeanCms(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
@@ -107,8 +115,10 @@ internal abstract class HeanCms(
source = source,
)
}
}
protected open val datePattern = "yyyy-MM-dd"
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@@ -172,7 +182,7 @@ internal abstract class HeanCms(
return tags.mapNotNullToSet {
MangaTag(
key = it.substringAfter("id\":").substringBefore(",\""),
title = it.substringAfter("name\":\"").substringBefore("\"}]"),
title = it.substringAfter("name\":\"").substringBefore("\"}]").toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -2,19 +2,10 @@ package org.koitharu.kotatsu.parsers.site.heancms.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) :
@@ -22,46 +13,56 @@ internal class YugenMangasEs(context: MangaLoaderContext) :
private val domainAlt = "yugenmangas.net"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domainAlt)
append("/query?query_string=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
}
append("&series_status=All&order=desc&orderBy=")
when (sortOrder) {
SortOrder.POPULARITY -> append("total_views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("created_at")
SortOrder.ALPHABETICAL -> append("title")
else -> append("latest")
}
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&tags_ids=")
append("[".urlEncoded())
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
// Just to make it fit [1,2,44] ect
if (!firstTag) {
firstTag = true
} else {
append(",")
}
append(tag.key)
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=desc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
else -> append("latest&order=desc")
}
append("&series_type=Comic&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("]".urlEncoded())
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/series/$slug"

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.parsers.site.heancms.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
@MangaSourceParser("REAPERSCANSPT", "ReaperScans.net", "pt")
internal class ReaperScansPt(context: MangaLoaderContext) :
HeanCms(context, MangaSource.REAPERSCANSPT, "reaperscans.net") {
override val configKeyDomain = ConfigKey.Domain("reaperscans.net", "reaperbr.online")
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.heancmsalt
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@@ -22,6 +23,8 @@ internal abstract class HeanCmsAlt(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val isSearchSupported = false
protected open val listUrl = "/comics"
protected open val datePattern = "MMMM d, yyyy"
@@ -33,23 +36,24 @@ internal abstract class HeanCmsAlt(
protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
protected open val selectMangaTitle = "h5"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
// No search or tag
if (!query.isNullOrEmpty()) {
return emptyList()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
}
null -> {}
}
if (page > 1) {
append("?page=")
append(page)
append(page.toString())
}
}
val doc = webClient.httpGet(url).parseHtml()
@@ -125,10 +129,10 @@ internal abstract class HeanCmsAlt(
}
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
protected open fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.startsWith("hace ") -> parseRelativeDate(date)
d.startsWith("hace ") || d.endsWith(" antes") -> parseRelativeDate(date)
else -> dateFormat.tryParse(date)
}
}
@@ -151,7 +155,7 @@ internal abstract class HeanCmsAlt(
)
}.timeInMillis
WordSet("mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("mes", "meses").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}

View File

@@ -0,0 +1,59 @@
package org.koitharu.kotatsu.parsers.site.heancmsalt.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.heancmsalt.HeanCmsAlt
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import java.text.SimpleDateFormat
@MangaSourceParser("BRAKEOUT", "Brakeout", "es")
internal class Brakeout(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaSource.BRAKEOUT, "brakeout.xyz", 10) {
override val selectManga = "div.grid.grid-cols-2 figure"
override val selectMangaTitle = "figcaption"
override val selectDesc = "#section-sinopsis p"
override val selectChapter = ".grid-capitulos div.contenedor-capitulo-miniatura"
override val selectChapterTitle = "#name"
override val selectChapterDate = "time"
override val datePattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
override suspend fun getDetails(manga: Manga): Manga {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return manga.copy(
altTitle = doc.selectFirst(selectAlt)?.text().orEmpty(),
description = doc.selectFirstOrThrow(selectDesc).html(),
chapters = doc.select(selectChapter)
.mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val dateText = div.selectFirstOrThrow(selectChapterDate).text()
val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain)
MangaChapter(
id = generateUid(url),
name = div.selectFirstOrThrow(selectChapterTitle).text(),
number = i + 1,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
branch = null,
source = source,
)
},
)
}
override val selectPage = "section img.readImg"
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.id
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -18,6 +19,84 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val headers: Headers = Headers.Builder()
.add("X-Requested-With", "XMLHttpRequest")
.add("Referer", "https://$domain/")
.build()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("manga")
addPathSegment("page")
addPathSegment("$page/")
when (filter) {
is MangaListFilter.Search -> {
addQueryParameter("title", filter.query)
}
is MangaListFilter.Advanced -> {
addQueryParameter("title", "")
addQueryParameter(
"order",
when (filter.sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "popular"
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
else -> "latest"
},
)
filter.tags.forEach {
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
}
filter.states.oneOrThrowIfMany()?.let {
addEncodedQueryParameter(
"statusx",
when (it) {
MangaState.ONGOING -> "Publishing"
MangaState.FINISHED -> "Finished"
else -> ""
},
)
}
}
null -> addQueryParameter("order", "update")
}
}.build()
return webClient.httpGet(url).parseHtml()
.requireElementById("archives")
.selectFirstOrThrow("div.entries")
.select(".entry")
.map {
val titleTag = it.selectFirstOrThrow(".metadata > a")
val relativeUrl = titleTag.attrAsRelativeUrl("href")
Manga(
id = generateUid(relativeUrl),
title = titleTag.attr("title"),
altTitle = null,
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive")
val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale)
@@ -58,56 +137,6 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("manga")
addPathSegment("page")
addPathSegment("$page/")
val order = when (sortOrder) {
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "popular"
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
else -> throw IllegalArgumentException("Sort order not supported")
}
addQueryParameter("order", order)
addQueryParameter("title", query.orEmpty())
tags?.forEach {
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
}
}.build()
return webClient.httpGet(url).parseHtml()
.requireElementById("archives")
.selectFirstOrThrow("div.entries")
.select(".entry")
.map {
val titleTag = it.selectFirstOrThrow(".metadata > a")
val relativeUrl = titleTag.attrAsRelativeUrl("href")
Manga(
id = generateUid(relativeUrl),
title = titleTag.attr("title"),
altTitle = null,
url = relativeUrl,
publicUrl = relativeUrl.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
.requireElementById("reader")

View File

@@ -39,26 +39,37 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
SortOrder.POPULARITY,
)
override val isMultipleTagsSupported = false
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp")
@InternalParsersApi
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val page = (offset / 20f).toIntUp().inc()
val domain = getDomain("seiga")
val url = when {
!query.isNullOrEmpty() -> return if (offset == 0) getSearchList(query, page) else emptyList()
tags.isNullOrEmpty() -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
tags.size == 1 -> "https://$domain/manga/list?category=${tags.first().key}&page=$page" +
"&sort=${getSortKey(sortOrder)}"
val url =
when (filter) {
is MangaListFilter.Search -> {
return if (offset == 0) getSearchList(filter.query, page) else emptyList()
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany().let {
"https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(filter.sortOrder)}"
}
} else {
"https://$domain/manga/list?page=$page&sort=${getSortKey(filter.sortOrder)}"
}
}
null -> "https://$domain/manga/list?page=$page"
}
tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 category")
else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
}
val doc = webClient.httpGet(url).parseHtml()
val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found")
val items = comicList.select("div > .description > div > div")
@@ -145,12 +156,12 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml()
val root = doc.body().selectOrThrow("#mg_category_list > ul > li")
val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1)
return root.mapToSet { li ->
val a = li.selectFirstOrThrow("a")
MangaTag(
title = a.text(),
key = a.attrAsRelativeUrlOrNull("href").orEmpty(),
key = a.attrAsRelativeUrl("href").substringAfter("category=").substringBefore("&"),
source = source,
)
}

View File

@@ -3,6 +3,8 @@ package org.koitharu.kotatsu.parsers.site.likemanga
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.json.JSONObject
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
@@ -22,44 +24,75 @@ internal abstract class LikeMangaParser(
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/?act=search&f")
append("[sortby]".urlEncoded())
append("=")
when (sortOrder) {
SortOrder.POPULARITY -> append("hot")
SortOrder.UPDATED -> append("lastest-chap")
SortOrder.NEWEST -> append("lastest-manga")
else -> append("lastest-chap")
append("/?act=search")
when (filter) {
is MangaListFilter.Search -> {
append("&f")
append("[keyword]".urlEncoded())
append("=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&f")
append("[sortby]".urlEncoded())
append("=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("hot")
SortOrder.UPDATED -> append("lastest-chap")
SortOrder.NEWEST -> append("lastest-manga")
else -> append("lastest-chap")
}
if (filter.tags.isNotEmpty()) {
append("&f")
append("[genres]".urlEncoded())
append("=")
filter.tags.oneOrThrowIfMany()?.let {
append(it.key)
}
}
filter.states.oneOrThrowIfMany()?.let {
append("&f")
append("[status]".urlEncoded())
append("=")
append(
when (it) {
MangaState.ONGOING -> "in-process"
MangaState.FINISHED -> "complete"
MangaState.PAUSED -> "pause"
else -> "all"
},
)
}
}
null -> {
append("&f")
append("[sortby]".urlEncoded())
append("=lastest-chap")
}
}
if (page > 1) {
append("&pageNum=")
append(page)
}
if (!tags.isNullOrEmpty()) {
append("&f")
append("[genres]".urlEncoded())
append("=")
append(tag?.key.orEmpty())
}
if (!query.isNullOrEmpty()) {
append("&f")
append("[keyword]".urlEncoded())
append("=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.card-body div.video").map { div ->
@@ -157,7 +190,8 @@ internal abstract class LikeMangaParser(
MangaChapter(
id = generateUid(url),
name = name,
number = chapNum.toInt(),
number = chapNum.toFloatOrNull() ?: 0f,
volume = 0,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
@@ -201,15 +235,39 @@ internal abstract class LikeMangaParser(
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(".reading-detail img").map { img ->
val url = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
val testJson = doc.selectFirst("div.reading input#next_img_token")
if (testJson != null) {
val jsonRaw = testJson.attr("value").split(".")[1]
val jsonData = JSONObject(context.decodeBase64(jsonRaw).toString(Charsets.UTF_8))
val jsonImg = context.decodeBase64(jsonData.getString("data")).toString(Charsets.UTF_8)
val images = jsonImg.replace("\\", "").replace("[", "").replace("]", "").replace("\"", "").split(",")
val baseUrl = doc.selectFirstOrThrow(".reading-detail img").src()
val cdn = baseUrl?.substringBefore("manga/", "")?.ifEmpty {
baseUrl.toHttpUrl().resolve("/").toString()
}
return images.map { img ->
val url = concatUrl(cdn.orEmpty(), img)
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
} else {
return doc.select(".reading-detail img").map { img ->
val url = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {

View File

@@ -33,6 +33,10 @@ internal abstract class MadaraParser(
SortOrder.RATING,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
protected open val tagPrefix = "manga-genre/"
protected open val datePattern = "MMMM d, yyyy"
protected open val stylePage = "?style=list"
@@ -149,79 +153,173 @@ internal abstract class MadaraParser(
"En attente",
)
@JvmField
protected val upcoming: Set<String> = hashSetOf(
"Upcoming",
"upcoming",
"لم تُنشَر بعد",
"Prochainement",
"À venir",
)
// Change these values only if the site does not support manga listings via ajax
protected open val withoutAjax = false
// can be changed to retrieve tags see getTags
protected open val listUrl = "manga/"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val doc = if (withoutAjax) {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (withoutAjax) {
val pages = page + 1
val url = buildString {
append("https://")
append(domain)
val pages = page + 1
if (!tags.isNullOrEmpty()) {
append("/$tagPrefix")
append(tag?.key.orEmpty())
if (pages > 1) {
append("/page/")
append(pages.toString())
when (filter) {
is MangaListFilter.Search -> {
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("/?s=")
append(filter.query.urlEncoded())
append("&post_type=wp-manga")
}
append("?")
} else {
append("/page/")
append(pages)
append("/?s=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("/$tagPrefix")
append(it.key)
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("/?")
}
} else {
if (pages > 1) {
append("/page/")
append(pages.toString())
}
append("/?s=&post_type=wp-manga")
filter.states.forEach {
append("&status[]=")
when (it) {
MangaState.ONGOING -> append("on-going")
MangaState.FINISHED -> append("end")
MangaState.ABANDONED -> append("canceled")
MangaState.PAUSED -> append("on-hold")
MangaState.UPCOMING -> append("upcoming")
}
}
filter.contentRating.oneOrThrowIfMany()?.let {
append("&adult=")
append(
when (it) {
ContentRating.SAFE -> "0"
ContentRating.ADULT -> "1"
else -> ""
},
)
}
append("&")
}
append("m_orderby=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
else -> append("latest")
}
}
null -> {
append("?s&post_type=wp-manga&m_orderby=latest")
}
append("&post_type=wp-manga&")
/// &status[]= ( on-going - end - canceled - on-hold - upcoming )
}
append("m_orderby=")
when (sortOrder) {
SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating")
}
}
webClient.httpGet(url).parseHtml()
return parseMangaList(webClient.httpGet(url).parseHtml())
} else {
val payload = if (sortOrder == SortOrder.RATING) {
val payload = if (filter?.sortOrder == SortOrder.RATING) {
createRequestTemplate(ratingRequest)
} else {
createRequestTemplate(defaultRequest)
}
when (sortOrder) {
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update"
SortOrder.NEWEST -> payload["vars[meta_key]"] = ""
SortOrder.ALPHABETICAL -> {
payload["vars[orderby]"] = "post_title"
payload["vars[order]"] = "ASC"
payload["page"] = page.toString()
when (filter) {
is MangaListFilter.Search -> {
payload["vars[s]"] = filter.query.urlEncoded()
}
SortOrder.RATING -> {}
}
payload["page"] = page.toString()
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
payload["vars[s]"] = query?.urlEncoded().orEmpty()
/// payload["vars[meta_query][0][0][value][]"] = ( on-going - end - canceled - on-hold - upcoming )
webClient.httpPost(
"https://$domain/wp-admin/admin-ajax.php",
payload,
).parseHtml()
}
is MangaListFilter.Advanced -> {
filter.tags.oneOrThrowIfMany()?.let {
payload["vars[wp-manga-genre]"] = it.key
}
when (filter.sortOrder) {
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update"
SortOrder.NEWEST -> payload["vars[meta_key]"] = ""
SortOrder.ALPHABETICAL -> {
payload["vars[orderby]"] = "post_title"
payload["vars[order]"] = "ASC"
}
SortOrder.RATING -> {}
else -> payload["vars[meta_key]"] = "_latest_update"
}
filter.states.forEach {
payload["vars[meta_query][0][0][value][]"] =
when (it) {
MangaState.ONGOING -> "on-going"
MangaState.FINISHED -> "end"
MangaState.ABANDONED -> "canceled"
MangaState.PAUSED -> "on-hold"
MangaState.UPCOMING -> "upcoming"
}
}
filter.contentRating.oneOrThrowIfMany()?.let {
payload["vars[meta_query][0][1][key]"] = "manga_adult_content"
payload["vars[meta_query][0][1][value]"] =
when (it) {
ContentRating.SAFE -> ""
ContentRating.ADULT -> "a%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22yes%22%3B%7D"
else -> ""
}
}
}
null -> {
payload["vars[meta_key]"] = "_latest_update"
}
}
return parseMangaList(
webClient.httpPost(
"https://$domain/wp-admin/admin-ajax.php",
payload,
).parseHtml(),
)
}
}
protected open fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.row.c-tabs-item__content").ifEmpty {
doc.select("div.page-item-detail")
}.map { div ->
@@ -249,6 +347,7 @@ internal abstract class MadaraParser(
in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED
in paused -> MangaState.PAUSED
in upcoming -> MangaState.UPCOMING
else -> null
},
source = source,
@@ -275,9 +374,9 @@ internal abstract class MadaraParser(
}
MangaTag(
key = href,
title = a.ownText().trim().ifEmpty {
a.selectFirst(".menu-image-title")?.text()?.trim() ?: return@mapNotNullToSet null
}.toTitleCase(),
title = a.ownText().ifEmpty {
a.selectFirst(".menu-image-title")?.textOrNull()
}?.toTitleCase(sourceLocale) ?: return@mapNotNullToSet null,
source = source,
)
}

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