1294 Commits

Author SHA1 Message Date
fe5534b006 Remove DUMMY parser 2025-09-07 09:48:55 +03:00
Sergey Shumov
6ca07aeff7 ZenManga: update site domain (#2168) 2025-09-06 10:11:46 +03:00
19567f9642 Update dependencies 2025-09-06 08:38:25 +03:00
4a854c7a23 Update docs 2025-09-06 08:38:24 +03:00
Draken
312b617b95 Cứu Truyện: Use another publicUrl for better redirection 2025-09-05 17:24:38 +07:00
0a0a869e88 Merge pull request #2166 from nakrovati/refactor/migrate-gradle-scripts-to-kotlin-dsl 2025-09-05 13:06:39 +03:00
Hoàng Phi Hùng
8573921243 GocTruyenTranhVui + KuroNeko: Fixes (#2165)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-09-05 15:16:43 +07:00
Draken
b8d3124227 Update Gradle 8.7 to 8.13 (#2133) 2025-09-04 13:33:25 +03:00
Sergey Shumov
14e23694fd FIx ZenManga manga page url (#2164) 2025-09-04 13:28:55 +03:00
Daniil Zhuravlev
7e25002dba refactor(build): migrate to Kotlin DSL and version catalog 2025-09-03 16:27:21 +04:00
Daniil Zhuravlev
a94d9bf8b8 chore: update .editorconfig 2025-09-03 15:15:54 +04:00
nakrovati
7430c7386e Mark broken parsers and update several domains (#2163)
* chore: update outdated domains

* chore: mark broken parsers
2025-09-03 05:56:53 +07:00
Naga
5f99e694c8 MediocreToons: Fix chapter order (#2162) 2025-09-02 22:21:43 +07:00
dragonx943
25b23854d8 Góc Truyện Tranh Vui: Add source 2025-09-02 21:57:54 +07:00
dragonx943
45d39fe94d KuroNeko: Add delay to avoid rate limit 2025-09-02 21:38:15 +07:00
dragonx943
21b63c1d77 TruyenQQ: Add delay to avoid rate limit 2025-09-02 21:27:10 +07:00
Draken
f64ce0f4a2 Dâm Cô Nương: Remove redundant escape character (#2160) 2025-09-02 16:39:55 +07:00
Naga
b3e7d8e8d5 SussyScan: Fix CDN url in getPages (#2156) 2025-09-01 13:33:58 +07:00
Hoàng Phi Hùng
8a147dbdd3 Update getpage for DamCoNuong parser (#2149)
* Update DamCoNuong.kt

* Update DamCoNuong.kt to fix "Can't find fallbackUrls"

* Update DamCoNuong.kt
2025-08-30 09:10:04 +03:00
Hoàng Phi Hùng
5a039acf82 Dâm Cô Nương: Fix unsupported format in getPages (#2148) 2025-08-30 02:37:14 +07:00
Draken
e9469ae0e0 Dâm Cô Nương: Update domain 2025-08-29 14:43:40 +07:00
Naga
48cfc3347b TraduccionesAmistosas: Update domain (#2145) 2025-08-29 14:37:26 +07:00
nakrovati
1566b807b7 Update domains and correct the source language (#2132) 2025-08-27 00:50:24 +07:00
Naga
8bed45dd2f Komikcast: Fix SortOrder (#2124) 2025-08-24 19:36:09 +07:00
Waranim
36c3a88d63 ZenManga: Add source (#2123) 2025-08-24 11:01:38 +03:00
16f5129b69 [LibSocial] Fix Not found error 2025-08-24 10:47:21 +03:00
dragonx943
674290d029 MimiHentai: Add Broken, need to open WebView to read 2025-08-23 14:53:47 +07:00
bivaly
6cb6873fcb GhostScan: Update site domain (#2118) 2025-08-22 20:31:11 +07:00
fuckpdf
705a684cc2 UzayManga: Fix getPages (#2086) 2025-08-18 22:05:44 +07:00
Naga
de76ccf753 Weeb Central: Update tags selector (#2087) 2025-08-18 08:53:16 +07:00
Naga
cbd74dd7df AnimeSama: Add source (#2080)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-08-17 13:29:46 +07:00
dragonx943
7f39798a8c Yuri Garden: Add intercept, apply unscrambleImage for getPages 2025-08-17 04:23:41 +07:00
Naga
31999d97a0 Hentaman: Add source (#2078) 2025-08-17 02:01:45 +07:00
dragonx943
fd0df2414e MimiHentai: Use another UserAgent key, fix query in getListPage 2025-08-16 10:58:29 +07:00
Naga
a93140fc36 MediocreToons: Add source (#2077) 2025-08-16 09:59:03 +07:00
Naga
7c264b6af7 SussyScan: Fixes (#2076) 2025-08-16 09:57:37 +07:00
Naga
134656b835 InManga: Add source (#2073) 2025-08-16 03:30:50 +07:00
Naga
8964bb1584 Manhwalist.org: Update domain (#2072) 2025-08-16 03:25:53 +07:00
Naga
8bee71342b PhenixScans: Fixes (#2071) 2025-08-15 03:08:02 +07:00
Naga
a4824a3582 RaijinScans: Fixes (#2070) 2025-08-15 03:07:15 +07:00
Draken
6fb4cc608c ci: Update dependencies for actions (#2065) 2025-08-14 00:15:59 +07:00
dragonx943
12aa4479cd ci: Update dependencies for actions 2025-08-14 00:15:08 +07:00
Draken
bfdb10d002 Dâm Cô Nương: Fix exclude tags, CSS selectors for parseMangaList (#2064) 2025-08-13 23:50:53 +07:00
Naga
f61f5329e3 ArabsHentai: Add source (#2063) 2025-08-13 23:43:43 +07:00
dragonx943
0477fe0659 site/vi: Update sources domain + Favicons 2025-08-13 16:52:32 +07:00
dragonx943
6b3329436f Làng Geek: Set CHROME_DESKTOP as default UserAgent key 2025-08-13 16:08:42 +07:00
Draken
35fd5042a7 Làng Geek: Fix CSS selector for getPages, attempt 1 2025-08-13 02:46:23 +07:00
Naga
d366628a20 HeyToon: Add source (#2060) 2025-08-13 01:42:21 +07:00
Naga
5483453f35 RocksManga: Update site structure (#2059) 2025-08-13 01:30:23 +07:00
Naga
02ac1cb896 Ikiru: Update site structure (#2058) 2025-08-12 12:27:59 +07:00
dragonx943
1563783afe ci: Use another module for Gradle, try to improve build times 2025-08-11 11:38:55 +07:00
Naga
f5436977a9 MangaGg: Fix chapter list (#2056) 2025-08-11 11:07:24 +07:00
dragonx943
b58cf32b7a ci: Upgrade actions version and split steps 2025-08-11 00:46:15 +07:00
dragonx943
38025a6b35 ci: Change Java version, use compileKotlin instead of assemble 2025-08-11 00:36:10 +07:00
Draken
96a2d9e0ac ci: Add workflow to automatically test and compile parsers 2025-08-11 00:23:03 +07:00
Draken
c9ebbfa1bf LxManga: Throw Exception for restricted content 2025-08-11 00:05:57 +07:00
Naga
91ec95448c MyReadingManga: Update site structure (#2054) 2025-08-11 00:00:06 +07:00
Naga
b5512e7574 [KdtScan] Update domain + Fixes (#2050)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
Co-authored-by: dragonx943 <premieregirl26@gmail.com>
2025-08-08 01:39:15 +07:00
Naga
8bc7480d84 [PerfScan] Update domain + Fixes (#2047) 2025-08-07 13:36:34 +07:00
Naga
db9a7161f4 [PoseidonScans] Add source (#2042)
Close #1641
2025-08-05 04:52:36 +07:00
74d0951d3d [Remanga] Fix chapters parsing 2025-08-04 15:57:25 +03:00
91d5eff20a [Remanga] Migrate to api v2 2025-08-04 15:35:39 +03:00
Draken
54d6b2a2bb [Làng Geek] Add source (#2041) 2025-08-04 03:20:38 +07:00
Naga
66cd27a673 [Manhastro] Fix chapter list (#2040)
Close #1376 #1362
2025-08-04 00:55:51 +07:00
Naga
137490001a [HuntersScan] Update domain and fixes (#2039)
Close #2038
2025-08-04 00:55:03 +07:00
Naga
c85dac5e0e [ManhuaScan] Fixes (#2037)
Close #1418 #1730 #1922
2025-08-03 11:36:20 +07:00
dragonx943
9a9102fa33 [LXManga] Small fix for scanlator 2025-08-03 02:06:24 +07:00
Naga
0d31adb20b [HentaiRead] Fix search, tags, rating (#2036)
Close #1945
2025-08-03 01:22:42 +07:00
Draken
df7f2d1a89 [LXManga] Fix state + scanlator 2025-08-02 23:35:53 +07:00
dragonx943
eaca421132 [GocTruyenTranh] Use API instead of fixed tags function 2025-08-02 13:33:58 +07:00
Naga
5f1f2df7f0 [ManhwaBreakup] Add source (#2034)
Close #1967
2025-08-02 03:37:28 +07:00
Naga
ed21a5b397 [DoujinHentai] Fixes (#2030)
Close #1983
2025-08-01 03:22:29 +07:00
Draken
39703e3ebb [Ổ Truyện] Small fixes (#2027) 2025-07-31 15:16:38 +07:00
Naga
14cff0d651 [RokariComics] Add source (#2023)
Close #2008
2025-07-31 11:27:10 +07:00
Naga
af89f42251 [Komiku] Fixes (#2022)
Close #1986
2025-07-31 02:59:05 +07:00
Naga
036ce155ae [FlameComics] Fix image loading (#2020)
Close #2014
2025-07-31 00:22:52 +07:00
Naga
177d014b13 [Kiryuu] Update domain (#2021) 2025-07-31 00:22:26 +07:00
dragonx943
7c74ff6177 [EleceedTurkiye] Fix warn 2025-07-30 04:02:59 +07:00
Naga
b139586df5 [KomikIndo] Update domain and structure (#2017)
Close #723 #1466
2025-07-30 03:06:50 +07:00
dragonx943
15c7f97432 [HentaiVN.plus] Add author search 2025-07-30 00:35:21 +07:00
Naga
f22c8f9e7c [TemakiMangas] Small fixes
Close #1997
2025-07-29 23:52:59 +07:00
dragonx943
b24baf7d93 [site/vi] Update domain 2025-07-29 23:45:19 +07:00
Draken
98e0f6cd6c [Ổ Truyện] Add source (#2012) 2025-07-29 23:25:50 +07:00
Naga
e7316b5cd0 [InovaScanManga] Remake parser (Close #1988) 2025-07-29 23:05:44 +07:00
Naga
33a0a68ac7 [Klz9] Fixes (close #1949, #1433, #1455) 2025-07-29 20:27:06 +07:00
a83baf4c12 [Grouple] Handle restricted state 2025-07-29 15:29:06 +03:00
307f2090f6 [LibSocial] Improvements 2025-07-29 15:20:46 +03:00
Naga
e73d636979 [KomikCast] Fixes (Close #1995) 2025-07-29 12:23:01 +07:00
Naga
77fb7270ce [RimuScans] Update domain (Close #2005) 2025-07-29 12:19:15 +07:00
9d35f26252 Fix warnings (refactor) 2025-07-27 19:14:04 +03:00
4eeb879451 Fix build 2025-07-27 19:04:18 +03:00
06aa701ab1 Update MangaState enum 2025-07-27 18:03:38 +03:00
Draken
3c55b68beb [EleceedTurkiye] Add source (#2006) 2025-07-27 15:00:08 +07:00
dragonx943
63f37d159a [MimiHentai] Fix sort orders 2025-07-26 22:45:43 +07:00
dragonx943
cb74c43f71 [MimiHentai] Temporarily remove "POPULARITY_MONTH" 2025-07-26 22:21:53 +07:00
dragonx943
798b7570fe [MimiHentai] Update favicon 2025-07-26 22:13:04 +07:00
Draken
9197b82e0d [KuroNeko] Rename source 2025-07-23 11:31:20 +07:00
Draken
ad45480439 [BatCave] Small fix for getDetails (#1994) 2025-07-22 16:35:13 +07:00
Draken
9493ff1630 [MyReadingManga] Fix tags + search 2025-07-22 14:24:08 +07:00
Draken
8ee46ad2d1 [TruyenHentai18] Small tweak + Refactor (#1992) 2025-07-22 12:15:22 +07:00
dragonx943
ba1b35522c [TruyenHentai18] Fix attempt 1 2025-07-22 11:54:47 +07:00
dragonx943
41c37c025a [site/vi] Update domain 2025-07-22 00:36:16 +07:00
Draken
9995657da5 [TruyenHentai18] Fix getPages 2025-07-21 23:30:33 +07:00
dragonx943
1569275cd4 [TruyenHentai18] Remove unused import + Broken 2025-07-21 23:27:08 +07:00
dragonx943
352d998680 [TruyenHentai18] Fix attempt 1 2025-07-21 23:12:24 +07:00
Draken
d0b2002a1b [KuroNeko + CuuTruyen] Small fixes (#1989) 2025-07-21 22:52:43 +07:00
5962efb7df Update parsers structure (again) 2025-07-18 13:50:02 +03:00
Draken
8b18d1f0f8 [Com-X] Update domain 2025-07-17 16:38:16 +07:00
Draken
97ca214815 [DoujinDesu.click] Update domain 2025-07-15 11:00:42 +07:00
Draken
5d9b56296b [HentaiVn.buzz + YuriGarden] Fixes (#1968)
* [HentaiVn.buzz] Fix "Nothing found" errors

* [YuriGarden] Add author search support + Small fixes
2025-07-14 20:52:51 +07:00
Batu
66f25c30fc [Gafeland] Add source (#1963)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-07-14 01:55:58 +07:00
dragonx943
fd197de86a [LXManga] Fix CSS selector in altTitles 2025-07-14 00:33:13 +07:00
dragonx943
5f7478b33c [LXManga] Store altTitles + Revert CSS selector 2025-07-13 23:53:26 +07:00
dragonx943
b058477b67 [LXManga] Small fix for altTitles 2025-07-13 23:12:57 +07:00
dragonx943
149d9e5a81 [LXManga] Remove not null constraint for altTitles 2025-07-13 19:37:05 +07:00
Batu
6420def95a [site/tr] Update domain + Add Broken (#1956)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-07-13 18:33:51 +07:00
fuckpdf
5dae26ce0b [MangaTilkisi] Add source (#1940)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-07-13 00:45:30 +07:00
Draken
b5fddadb18 [LXManga] Small fixes for getDetails (#1955) 2025-07-13 00:44:29 +07:00
Draken
613d1cd1a1 [PhenixScans] Fix API path 2025-07-10 16:54:31 +07:00
fuckpdf
a6a72064bb [TenshiManga] Add source (#1933)
* [UzayManga + ElderManga] Update order

* [TenshiManga] Add source

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-07-10 14:26:49 +07:00
dragonx943
73c0a4f606 [YuriGarden] Revert scanlator for getDetails 2025-07-10 14:23:03 +07:00
Draken
1d2ee20d17 [YuriGarden] Fixes (#1934) 2025-07-10 14:09:09 +07:00
dragonx943
67b556c2eb [YuriGarden] Small fixes 2025-07-08 18:05:31 +00:00
Draken
3962c87e1d [YuriGarden] Fix line 2025-07-09 00:51:41 +07:00
dragonx943
e6ca455254 [YuriGarden] Fix order / path + Remove rating 2025-07-08 17:49:08 +00:00
Naga
d481ff416f [Flamecomics] Fix search and genre fitlering (#1923) 2025-07-08 02:22:16 +07:00
Draken
b0236e492d [YuriGarden] Add source (#1924) 2025-07-08 02:01:53 +07:00
dragonx943
497321bfa8 [KuroNeko] Add author search support 2025-07-07 12:29:02 +07:00
Draken
eff26f6303 [CBHentai] Add author search + Fix duplicate tags (#1918) 2025-07-06 17:53:26 +07:00
Crono
b5bd5b8cdf [Manhastro + LuraToon] Update domain (#1915) 2025-07-05 22:46:49 +07:00
Draken
4cc15daba3 [Koharu] Add Broken 2025-07-05 22:43:56 +07:00
248d51321a [Koharu] Refactor UserAgent 2025-07-05 08:41:29 +03:00
7cb9a264de [LibSocial] Allow to change domain 2025-07-05 08:39:47 +03:00
Draken
b58e9ff328 [TopTruyen] Update domain 2025-07-05 01:24:02 +07:00
dragonx943
958e0bed5c [site/vi] Update domain 2025-07-05 00:47:40 +07:00
ViAnh
e8d4143d72 [Koharu] Fix getPages (#1914)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-07-05 00:19:00 +07:00
Naga
079b2346f1 [Webtoon] Fixes (#1912) 2025-07-03 20:56:20 +07:00
Iwakurra
553615b3ff [HentaiLib] Update domain + allow user to set custom (#1908) 2025-07-02 23:47:50 +07:00
f8992457dc Update domains 2025-06-29 16:26:27 +03:00
Draken
06a9209b9e [DexHentai] Fix getListPage (close #1901) 2025-06-26 12:43:42 +07:00
Draken
4c0b873013 [Hentalk] Update domain 2025-06-23 22:08:19 +07:00
dba7f46f73 [Remanga] Update domain (#1891) 2025-06-21 19:23:01 +03:00
8665186426 [LibSocial] Fix chapters branches 2025-06-21 19:18:59 +03:00
b5c6a94311 [ExHentai] Fix pagination 2025-06-21 19:08:27 +03:00
1540021f6f [Remanga] Fix parsing manga without covers 2025-06-21 19:03:06 +03:00
0e1247476e [LibSocial] Fix tags in details 2025-06-21 18:55:57 +03:00
SrCrono
bf83909349 [FlowerManga] Update domain (#1890) 2025-06-19 12:20:38 +07:00
Draken
e398a01f14 [AgsComics] Fix cover images
Close #1886
2025-06-19 03:34:26 +07:00
Draken
0e77938ade [NewTruyen] Fix getPages
Solved #1604
2025-06-19 02:17:34 +07:00
dragonx943
a4a9a1cf67 [NewTruyen] Fix getPages 2025-06-19 02:10:37 +07:00
Draken
3f9544fdb8 [MimiHentai] Small fix for exclude tags 2025-06-18 23:03:36 +07:00
dragonx943
bd30fc35a1 [WestManga] Add broken 2025-06-16 22:38:43 +00:00
Draken
b6f82cc282 Merge pull request #1884 from dragonx943/dev
[1stKissManga] Update domain + Small fix
2025-06-17 05:36:43 +07:00
dragonx943
9222079e98 [1stKissManga] Update domain + Small fix 2025-06-16 22:31:14 +00:00
Draken
49158b99d5 [CManga] Fix logic 2025-06-16 05:14:28 +07:00
Draken
3fe61a09b7 [CManga] Improvement getPages 2025-06-16 04:27:34 +07:00
Draken
b82ad1356e [CManga] Fix chap number (#1880) 2025-06-15 18:46:05 +07:00
dragonx943
b825d06b22 [CManga] Fix chap number 2025-06-15 11:27:38 +00:00
Draken
28f5856f9e [Desu] Update domain 2025-06-15 03:22:19 +07:00
Draken
19751674ee [TruyenHentai18] Add broken 2025-06-15 03:15:46 +07:00
Draken
a78afba15e [KuroNeko + TruyenHentai18] Fixes (#1879)
Solved #1683 #1604
2025-06-15 00:00:17 +07:00
Draken
0e946cf84c [site/vi] Update sources domain (#1874) 2025-06-14 23:59:18 +07:00
Draken
6cd29603ac [CBHentai] Fix duplicated tags (#1873) 2025-06-09 20:49:33 +07:00
Draken
d94cf53624 [MimiHentai] Add exclude tags support (#1872) 2025-06-09 20:18:42 +07:00
Draken
c81628c027 [YuriGarden] Removal (#1866, close #1863) 2025-06-07 15:50:52 +07:00
bcde8ef2a2 Fix build 2025-06-07 11:15:29 +03:00
66df6caa67 [ExHentai] Improvements 2025-06-07 10:53:28 +03:00
Draken
08e53b2f5f [MimiHentai] Small fix 2025-06-05 17:33:56 +07:00
Draken
7c83730ad0 [MimiHentai] Fix getListPage 2025-06-05 16:23:56 +07:00
dragonx943
d2a026c054 [MimiHentai] Remove state 2025-06-05 08:41:55 +00:00
dragonx943
faf8f6ffd7 [MimiHentai] Fix getListPage 2025-06-05 08:35:23 +00:00
Draken
6c372548db [site/vi] Update sources domain (#1861) 2025-06-05 09:23:37 +07:00
Draken
0029697350 Fix line 2025-06-05 09:21:22 +07:00
dragonx943
bdcf27b955 [site/vi] Update sources domain 2025-06-05 02:20:12 +00:00
Draken
bb7ac26c0c [MimiHentai] Small fixes (#1860) 2025-06-05 09:03:45 +07:00
Draken
7b42317996 [MimiHentai] Add Broken 2025-06-05 05:14:06 +07:00
Draken
103b0e7eb6 [ZinChanManga] Update domain
Solved https://github.com/KotatsuApp/kotatsu-parsers/issues/1835
2025-06-05 03:33:13 +07:00
Draken
fa80598e7c [DamCoNuong] Update domain
Solved a task in #1604
2025-06-03 02:58:28 +07:00
Draken
3cb5367028 [madtheme/en + wpcomics/vi] Small fixes (#1830)
* [WpComics] Split NetTruyenX + Small improvements

* [MangaBuddy] Add subdomain

* [MangaPuma] Add subdomain

* [MangaForest] Add subdomain

* [MangaCute] Add subdomain

* [Manhuagui] Change UA to `CHROME_DESKTOP`

* [Mangaxyz] Add subdomain
2025-06-03 01:11:42 +07:00
Zhifan Ye
9417d1f973 [Manhuagui] Fix UA and parse errors (#1834) 2025-06-03 01:08:26 +07:00
mozzaru
173d30737b [MgKomik] Update domain (#1828) 2025-06-01 00:53:59 +07:00
Draken
0db45877b7 [SolarMTL] Add source (#1826) 2025-05-31 18:12:40 +07:00
Draken
e7fac812a7 [SnowMTL] Add source (#1823)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-31 12:29:13 +07:00
Draken
ab26e6b3d0 [TrueManga] Update domain 2025-05-30 15:19:21 +07:00
Draken
baafc29967 [MimiHentai] Small fix 2025-05-28 15:12:03 +07:00
Draken
b0bd3e99a4 [MimiHentai] Small changes (#1818)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-28 03:34:22 +07:00
Draken
6bf377a654 [NhatTruyenVN] Fix list chaps (#1816)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-27 14:46:48 +07:00
Zhifan Ye
3d78b9003d [Manhuagui] Add source (#1812)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-27 02:48:57 +07:00
Draken
f57722ec54 [Com-X] Small changes (#1814)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-26 16:18:40 +07:00
9558a34b00 [Koharu] Fixes 2025-05-25 19:29:37 +03:00
Draken
522e3af41d [Koharu] Fixes (#1811)
* [Koharu] Debug attempt 1

* [Koharu] Debug attempt 2

* [Koharu] Debug attempt 3

* [Koharu] Fix chapter url

---------

Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-25 10:06:52 +03:00
7b1a0b8d0d [LibSocial] Authorization support 2025-05-24 16:50:23 +03:00
5856681753 [LibSocial] Fixes 2025-05-24 14:04:01 +03:00
Draken
f066be47fe [site/vi] Update domain + Small fixes (#1804)
* [VioletScans] Move to "en"

* [site/vi] Update sources domain

* [Madara] Split and allow overwrite for "postData"

--------------------------

Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-24 16:35:14 +07:00
Draken
884142a15e [UlasComic] Update domain + Fix getPages (#1803) 2025-05-24 15:22:37 +07:00
Tronik568
d7267b20fd [MaidScan] Update domain (#1802) 2025-05-23 14:46:45 +07:00
Talkc0n
0537cdd533 [SayHentai] Update domain (#1801) 2025-05-23 01:07:06 +07:00
Draken
c5f939df70 [Com-X] Small fixes: Bypass age verification (#1800) 2025-05-22 15:11:55 +07:00
Darwin
6854164e5a [VioletScans] Add source (#1798)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-21 20:39:14 +07:00
Draken
1e942c650b [HiperDex] Update domain + Remove Broken
Solved #1766
2025-05-21 15:21:30 +07:00
Naga
30a06b67d8 [Hitomi.La] Fix cover images (#1795) 2025-05-21 03:28:52 +07:00
Naga
34a1dabae4 [MagusToon] Move to iken (#1794)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-21 01:15:05 +07:00
Draken
23227a0427 [HoloEarth] Fix getDetails + Add locale (#1792)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-20 13:19:58 +07:00
Draken
02d1aef77c [Multporn] Add source (#1791)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-20 11:43:53 +07:00
Darwin
39ebdacef9 [HoloEarth] Add source (#1788)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-20 10:14:36 +07:00
Draken
276349e23c [YuriGarden] Replace SAFE with SUGGESTIVE 2025-05-18 00:38:17 +07:00
kerimmkirac
0507ed8d59 [DemonicScans] Add source (#1785)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-17 19:36:49 +07:00
Nam Huynh
2f8a0fe775 [HentaiNexus + HentaiRead] Small fixes (#1784)
* [HentaiNexus]
+ Fix package (due to moving)
+ Fix getPages() getPageUrl() (add flag for data reloading)
+ Fix getDetails (tag with quotes)
+ Cleaning

* [HentaiRead]
+ Fix package (due to moving)
+ Fix getListPage() (author searching)
+ Re-write getDetails() (update source)
+ Cleaning
2025-05-17 14:28:59 +07:00
Draken
aab7e9360a [site/vi] Update sources domain (#1780)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-15 21:23:49 +07:00
Draken
9db443f7f7 [HangTruyen + GocTruyenTranh] Fix getPages (#1778) 2025-05-15 19:46:39 +07:00
Nam Huynh
70ce9eb008 [HentaiNexus + HentaiRead] Small fixes (#1776)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-14 13:36:32 +07:00
Draken
a58947376c [Misskon] Add source (#1772) 2025-05-10 20:20:13 +07:00
Draken
cbb70f94dd [Manhwa210] Add source (#1771) 2025-05-10 19:05:46 +07:00
9408b9ba1b Update MangaLoaderContext 2025-05-10 13:33:11 +03:00
409828d22f Update MangaLoaderContext 2025-05-10 12:06:20 +03:00
Nam Huynh
c9d32a804c [Nhentai World + MeHentaiVN] Fixes (#1769)
* [HentaiRead] Fix lines

* [MeHentaiVN] Soft re-write due to source changed (#1604)

* [NHentaiWorld] Fixes fetchTags (due to source changed) and pass MangaParserTest:link (#1604)

* [MeHentaiVN] Small changes

* [MeHentaiVN] Update domain

---------

Solve task #1604

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-10 14:28:18 +07:00
Draken
a65cbf4981 [ReaperComics] Add Broken 2025-05-10 13:44:49 +07:00
mozzaru
e325063260 [Apkomik] Add Source (#1765) 2025-05-09 17:19:25 +07:00
cf0177364c Add requestDataFromBrowser function to MangaLoaderContext 2025-05-08 14:06:52 +03:00
Draken
8eaad2270b [Hitomi.La] Small fix for red covers 2025-05-08 12:54:15 +07:00
Nam Huynh
362f9c6786 [HentaiRead] Add Source! (KotatsuApp#1745) (#1762)
Solved https://github.com/KotatsuApp/kotatsu-parsers/issues/1745

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-08 00:33:35 +07:00
Draken
3047949b22 [ru/grouple] Small fixes (#1761)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-07 14:08:31 +07:00
Draken
8d7c81c8ef Merge branch 'master' into master 2025-05-07 13:19:33 +07:00
Nam Huynh
7d6e12d1fc [HentaiNexus] Add source (#1757)
Solved #1690

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-06 23:05:55 +07:00
spozer
7a0f1af06a [ReadOnePiece] Add source (#1758)
Solved https://github.com/KotatsuApp/kotatsu-parsers/issues/1040

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-06 22:51:19 +07:00
Draken
21aaa23586 [MimiHentai] Remove deprecated value 2025-05-06 22:00:34 +07:00
Draken
95fcbd44fd [Kiutaku] Debug attempt 1 2025-05-05 14:22:59 +00:00
Draken
cc98931147 [Kiutaku + Xiutaku] Add sources (#1755)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-05 13:11:12 +07:00
Draken
8f61beadb2 [Hitomi.la] Small fixes (#1754)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-05 13:03:45 +07:00
Draken
6d404c6997 [RoliaScan + ElderManga] Add sources (#1753) 2025-05-05 13:02:57 +07:00
kerimmkirac
27060cfef0 [RoliaScan + ElderManga] Add sources (#1753)
* [RoliaScan] Add source

* [ElderManga] Add source

* [UzayManga] Fix search

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-05-05 13:02:20 +07:00
Draken
ca3d8f555a [BatCave] Remove referer 2025-05-04 16:16:58 +07:00
Draken
7ea59a7bf1 [HeanCms] Small improvements (#1751)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-04 16:01:34 +07:00
Draken
cea4db9619 [BatCave] Small fixes (#1750)
Solved #1749
2025-05-04 16:01:14 +07:00
Draken
e7c3168099 [Readmanga] Add referer (#1748) 2025-05-04 16:00:38 +07:00
Draken
b165a0d611 Merge pull request #1744 from dragonx943/debug
[TeamXNovel] Small fixes
2025-05-03 11:20:41 +07:00
Draken
43e6c7c86f [TeamXNovel] Small fixes 2025-05-03 04:17:01 +00:00
Draken
346828402b [TeamXNovel] Revert: Fix getPages 2025-05-03 11:07:33 +07:00
Draken
01d67eca2e [site/vi] Update sources domain (#1741)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-05-03 00:16:56 +07:00
Draken
4445b666aa [site/iken] Small fixes (#1740)
* Vortexscans (Broken): Need to fix list chaps + getPages function

* Temporary fix for #1737, reference: keiyoushi/extensions-source#8645
2025-05-01 17:08:32 +07:00
Draken
adf0b4e397 [DayComics] Fix list chaps 2025-05-01 09:57:39 +07:00
Draken
e797cabd88 [HotComics] Fix list chaps (#1739)
Solved https://github.com/KotatsuApp/kotatsu-parsers/issues/1717, reference: https://github.com/keiyoushi/extensions-source/pull/5405
2025-05-01 09:53:20 +07:00
Draken
0c32b56378 [NetTruyen1975] Add source (#1736)
* [UzayManga] Fix tags (Attempt 1)

* [NetTruyen1975] Add source

* [NetTruyen*] Fix getPages

---------

Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-04-30 00:12:23 +07:00
kerimmkirac
5c9c2c3a7a [BookManga] Add Source (#1731) 2025-04-28 01:46:34 +07:00
kerimmkirac
7cd7180adb [ArmoniScans] Add Source (#1729)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-04-27 20:02:44 +07:00
kerimmkirac
4b6b585801 [MajorScans] Rename + Update domain (#1725) 2025-04-27 00:34:09 +07:00
Draken
1cf2153df0 [UzayManga] Fixes (#1726) 2025-04-27 00:33:13 +07:00
Draken
6ed104aeca [AlucardScans] Add source (#1722) 2025-04-27 00:00:47 +07:00
kerimmkirac
9ba1629065 [AlucardScans] Add source (#1722) 2025-04-26 23:59:48 +07:00
kerimmkirac
669709dfbb [site/tr + en] Add new sources (#1721)
* Add UzayManga, Mangacix, AsuraScansTR, TrMangaOku, MangaKoleji, HiveComic

* [Mangacix] Clean url

* [HiveComic] Add Broken

* [UzayManga] Clean up

* Update summary.yaml

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-04-26 21:54:53 +07:00
kerimmkirac
b5cb310aab [RagnarScans] Add source (#1715)
* Add files via upload

* Update summary.yaml

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Update Ragnarscans.kt

* Remove old parser

* [Ragnarscans] Add source

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-04-26 09:38:18 +07:00
Draken
50771771a8 [TeamXNovel] Fix getPages
Reference: https://github.com/keiyoushi/extensions-source/pull/8624
2025-04-25 21:35:59 +07:00
Draken
dce24e2005 [MimiHentai] Small change 2025-04-25 15:43:11 +07:00
Draken
d3a61f6556 [MimiHentai] Fix chaps order (#1711)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-04-25 15:19:58 +07:00
Dika Ardianta
8cdd7effe5 [site/id] Update sources domain + Rename (#1705)
- Update sources domain
- Remove dead sources
- Remove duplicate site
- Rename title ManhwaList.in to ManhwaList
- Rename title KomikMirror to KomikLokal

* restore removed files with @Broken marker

Related to commit 60a085843d
2025-04-25 14:17:47 +07:00
kerimmkirac
e389b01d35 [MangaGezgini] Update domain (#1710) 2025-04-25 01:55:31 +07:00
Draken
2c9632e3e5 [site/vi] Update sources domain (#1709) 2025-04-24 23:25:14 +07:00
Draken
2fff92b023 [HangTruyen] Add source (#1708)
Solved task #1694
2025-04-24 23:05:44 +07:00
Draken
c949b95dab [YuriGarden] Add source (#1707)
Solved task #1704
2025-04-24 17:13:14 +07:00
Draken
da145c7f41 [MangaDistrict] Small change 2025-04-23 14:13:44 +07:00
bivaly
16b8bf9328 [FlameComics] Fixing coverurl (#1697) 2025-04-22 03:10:05 +07:00
f076c8095a Misc parsers fixes 2025-04-21 12:22:08 +03:00
d2b2578a3a [Shinigami] Fixes 2025-04-21 12:09:09 +03:00
b24741678c [Koharu] Fixes 2025-04-21 11:18:49 +03:00
dragonx943
fecb1db3be [Shinigami] Fix search with tags + Refactor 2025-04-20 14:21:04 +07:00
Draken
6418422157 [site/vi] Update sources domain 2025-04-20 00:56:11 +07:00
Draken
7718ff28e7 [Shinigami] Remake parser
* Solved task #1676

* Solved #1654 #1638 #1563 #1535 #1402 #1380 #1306 #1289 #1089
2025-04-20 00:16:29 +07:00
devi
3771b8d26b Fix PhenixScans close #1599
close #1675
Fix KeyoappParser close #1640
2025-04-18 20:20:42 +02:00
Draken
c1357b46fb [NetTruyen] Fix list chaps (#1678) 2025-04-18 00:39:08 +07:00
Draken
6c9f6eaae5 [Shinigami] Add Broken 2025-04-17 12:26:16 +07:00
Draken
309b6405f9 [Koharu] Add source (#1667)
[Koharu] Add source (Broken)
2025-04-16 03:52:20 +07:00
Draken
9058ead462 [Com-X] Add source (#1673) 2025-04-16 03:46:09 +07:00
Draken
162436c913 [BatCave] Fix search (#1672) 2025-04-16 00:59:25 +07:00
Draken
876f3a3d3a [BatCave] Move to Comic sources 2025-04-15 23:40:20 +07:00
Draken
02acc7e9d4 [CuuTruyen] Add new SortOrder (#1669) 2025-04-14 15:46:45 +07:00
e874837efb Improve some parsers 2025-04-13 09:43:31 +03:00
Draken
c294f5bb61 [Hentalk] Small fixes 2025-04-13 03:33:20 +07:00
Draken
826c948260 [Hentalk] Add source (#1665) 2025-04-13 02:53:19 +07:00
Draken
a5c70f0b51 [WaManga] Set reverse for list chaps 2025-04-13 01:25:24 +07:00
Draken
c2f9c18e29 [YuriNeko] Add Broken 2025-04-12 03:24:18 +07:00
Draken
91cd239c97 [BatCave] Add source (#1658) 2025-04-12 03:06:24 +07:00
Draken
4d255d0d53 [MimiHentai] Add source (#1651)
* [MimiHentai] Add source

* [MimiHentai] Fixes

* [MimiHentai] Clean code

---------

Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-04-11 23:34:58 +07:00
Draken
d5a1e1a52f [site/vi] Update sources domain (#1653)
Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-04-11 23:34:32 +07:00
9abc80880f [Desu] Update domain 2025-04-10 09:29:51 +03:00
Draken
8d91adbd6e [Vcomycs] Update domain - #1644 2025-04-09 01:45:36 +07:00
kerimmkirac
20a24db949 Fixing url of turkish extensions (#1642)
* Update MangaGezgini.kt

* Update MangaTr.kt

* Update Timenaight.kt

* Update Mangacim.kt

* Update MilaSub.kt

* Update Grimelek.kt

* Update Grimelek.kt

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-04-08 08:03:26 +07:00
b837659408 [Desu] Fix domains order 2025-04-06 16:30:49 +03:00
MakaMonstir
696e0bc8f6 [Desu] Update domain (#1637)
* Update domain list

* [Desu] Update domain

---------

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
2025-04-05 23:20:54 +07:00
palaks-1
2ec2484982 [NHentai.xxx] Fix source (#1628) 2025-04-01 22:31:10 +07:00
Draken
738e2649d6 [DocTruyen5s] Small fixes 2025-04-01 18:20:50 +07:00
Draken
8e9da4e39f [Hitomi.La] Change cdn domain & fix image url logic (#1624)
* [Hitomi.La] Change cdn domain & fix image url logic

* [Hitomi.La] Set null for preview in getPages

* [Hitomi.La] Remove unused function

* [Hitomi.La] Use webp for preview images

* [Hitomi.La] Fix build
2025-03-31 15:30:38 +07:00
Draken
da3a9a7d9b [Phenixscans] Mark as Broken 2025-03-31 13:27:37 +07:00
Draken
8f9c0d93f6 [DocTruyen3Q] Fix chapter images + Improvements (#1621) 2025-03-30 16:41:12 +07:00
Draken
8bb0c4f4f1 [site/vi] Update sources domain 2025-03-30 00:27:50 +07:00
dbb04d2051 Fix link resolver 2025-03-28 09:03:36 +02:00
Draken
84562d7b36 [DoujinDesu.click] Update domain
Solved #1590
2025-03-25 13:10:14 +07:00
Draken
29327dafd0 [DamCoNuong] Update domain + Fix images
* [DamCoNuong] Update domain + Fix images
* [DamCoNuong] Fix domain
2025-03-24 18:43:48 +07:00
Draken
c0d12b0c83 [CuuTruyen] Fixes + Tweaks
* [Cứu Truyện] Fix chap scanlator

* [Cứu Truyện] Fix search with state + Tags
2025-03-24 17:57:50 +07:00
a0041f3c93 Fix build 2025-03-21 19:06:26 +02:00
2175e91db7 [DamCoNuong] Fixes 2025-03-21 18:44:59 +02:00
Draken
5fa7590550 [TopTruyen] Update domain 2025-03-21 18:25:34 +07:00
Draken
f3ae64ed1f [HentaiZ] Update domain 2025-03-21 18:24:52 +07:00
cc437b8cd4 [ExHentai] Pages preview support 2025-03-21 13:16:09 +02:00
Draken
db0782c61a Merge pull request #1591 - dragonx943/damconuong/fixes
[DamCoNuong] Fixes
2025-03-21 18:11:28 +07:00
Draken
bbf059ff35 [DamCoNuong] Fixes 2025-03-21 18:07:18 +07:00
Draken
bebc615376 Merge pull request #1589 - dragonx943/sources/damconuong
[DamCoNuong] Add source
2025-03-21 02:58:40 +07:00
Draken
77616a5404 [DamCoNuong] Add source 2025-03-20 19:55:05 +00:00
Draken
45584f2eea Merge pull request #1588 - dragonx943/fixes
[TopTruyen] Fix chapter images
2025-03-20 22:15:27 +07:00
dragonx943
1c3e59f9f0 [TopTruyen] Fix chapter images 2025-03-20 21:16:09 +07:00
Draken
a88de5b1b6 [TopTruyen] Small changes 2025-03-20 21:02:43 +07:00
Draken
91977ba025 [MyReadingManga] Small changes 2025-03-20 21:01:31 +07:00
Draken
5420290564 [CuuTruyen] Add some tags 2025-03-17 23:32:12 +07:00
Draken
98c21db8da Merge pull request #1577 - dragonx943/sources/domain
[vi/site] Update sources domain
2025-03-17 20:11:01 +07:00
dragonx943
23953cae11 [vi/site] Update domain 2025-03-17 20:06:41 +07:00
Draken
9cd310ba51 Merge pull request #1576 - dragonx943/sources/doctruyen3q
[TopTruyen + DocTruyen3Q] Fix chapter images
2025-03-17 19:50:42 +07:00
dragonx943
51311a17fb [TopTruyen] Fix chapter images 2025-03-17 19:46:40 +07:00
dragonx943
6799d79754 [DocTruyen3Q] Fix chapter images 2025-03-17 19:24:42 +07:00
Draken
e429cdd811 [WeebCentral] Fix title not found 2025-03-17 18:07:36 +07:00
Draken
2e5ce98bac Merge pull request #1575 - dragonx943/sources/myreadingmanga
[MyReadingManga] Add source
2025-03-17 17:24:48 +07:00
Draken
68c3a7148f [MyReadingManga] Add source 2025-03-17 10:08:10 +00:00
Draken
2a1c5bc6da [Manga18] Fix title not found 2025-03-17 00:14:18 +07:00
Draken
6fd18543ef [Manga18] Fix title not found 2025-03-17 00:11:38 +07:00
ae76631aaf [HeanCms] Fix manga ids 2025-03-16 13:04:34 +02:00
Draken
ca59e8e577 Merge pull request #1571 - dragonx943/sources/cmanga
[CManga] Small fixes
2025-03-15 15:27:18 +07:00
Draken
3648b36d16 [CManga] Small fixes 2025-03-15 15:13:33 +07:00
Draken
439b7862a3 Merge pull request #1570 - dragonx943/sources/doctruyen5s
[DocTruyen5s] Fixes
2025-03-15 14:41:03 +07:00
dragonx943
f4b975c865 [DocTruyen5s] Fixes 2025-03-15 14:38:34 +07:00
dragonx943
da02a82d74 [DocTruyen5s] Fixes 2025-03-15 14:24:01 +07:00
bivaly
0d5498e60d Update FlameComics.kt 2025-03-15 08:13:36 +02:00
0847baf17b Fix http request processing 2025-03-14 20:55:36 +02:00
adf5794ad3 Expose authorization provider 2025-03-14 19:18:25 +02:00
Draken
cfeea09a05 Merge pull request #1567 - dragonx943/sources/domain
[site/vi] Update domain + Tweak
2025-03-14 18:17:11 +07:00
dragonx943
e9bcb1abed [site/vi] Update domain + Tweak 2025-03-14 18:13:30 +07:00
Draken
35cf827c90 Merge pull request #1566 - dragonx943/sources/nhentaiworld
[Nhentai World] Fix tags
2025-03-14 12:47:29 +07:00
dragonx943
a10c34115b [Nhentai World] Fix tags 2025-03-14 12:37:29 +07:00
Draken
2f4e93e391 Merge pull request #1565 - dragonx943/sources/nhentaiworld
[Nhentai World] Fix chapters list
2025-03-14 01:00:28 +07:00
dragonx943
6ed42bc28b [Nhentai World] Fix chapters list 2025-03-14 00:58:02 +07:00
dragonx943
dc7c859344 [Nhentai World] Fix chapters list 2025-03-14 00:56:19 +07:00
a82e41aabb Deprecate Manga.isNsfw 2025-03-12 13:01:43 +02:00
Draken
3c62f4280f Merge pull request #1557 from dragonx943/kuma/tweak
[Kumapage] Fixes + Tweak
2025-03-11 10:23:34 +07:00
Draken
f806cdd118 [Kumapage] Fixes + Tweak 2025-03-11 02:48:22 +00:00
Draken
224c8a6c53 [Kumapage] Fixes + Tweak 2025-03-11 02:43:17 +00:00
Draken
408be7fa3c Merge pull request #1555 - dragonx943/sources/kuma
[Kumapage] Add source
2025-03-10 21:46:27 +07:00
Draken
a02b001768 [Kumapage] Add source + TODO 2025-03-10 14:43:03 +00:00
Draken
5edbdf801a [Kumapage] Fixes 2025-03-10 14:41:21 +00:00
Draken
cc3701250a [Kumapage] Fixes 2025-03-10 14:27:07 +00:00
Draken
e94d434f92 [Kumapage] Fixes 2025-03-10 14:19:31 +00:00
Draken
4d3adaa143 [Kumapage] Add source 2025-03-10 13:51:22 +00:00
Draken
36eecf781f Merge pull request #1552 - palaks-1/source/fixes
[NHentai.xxx] Fixes
2025-03-10 01:11:38 +07:00
palaks-1
76f44c98be [NHentai.xxx] Fix source 2025-03-09 20:06:08 +02:00
Draken
d660d9a30f Merge pull request #1551 - dragonx943/sources/domain
[site/vi] Update sources domain
2025-03-10 00:38:48 +07:00
Draken
8326ee8c29 [site/vi] Update domain 2025-03-09 17:36:07 +00:00
Draken
690a004de6 Merge pull request #1550 - dragonx943/sources/dragon
[Dragon Translation] Remake parser
2025-03-10 00:22:50 +07:00
Draken
98c6454eba [Dragon Translation] Fix pages img 2025-03-09 17:14:48 +00:00
Draken
8e42ef1229 [Dragon Translation] Remake attempt 1 2025-03-09 16:58:25 +00:00
Draken
02359a2630 Fix build 2025-03-09 22:53:11 +07:00
Draken
158f6f4232 [Dragon Translation] Fix build 2025-03-09 21:52:14 +07:00
Draken
77de194b83 [Dragon Translation] Small changes 2025-03-09 17:16:29 +07:00
Draken
85cd13c8b2 Merge pull request #1549 - dragonx943/fixes
[MeHentaiVN] Fix "403 Access Denied" errors
2025-03-09 16:33:34 +07:00
Draken
bfbd01b1c2 [MeHentaiVN] Fixes 2025-03-09 16:30:13 +07:00
e83636edc0 [NhentaiWorld] Fixes 2025-03-09 10:20:39 +02:00
63dc67b6fd [NhentaiWorld] Draft 2025-03-09 09:55:19 +02:00
b0de4c3b66 [ComicK] Improvements 2025-03-09 09:42:02 +02:00
Draken
d5a4cf68c6 [NewTruyen] Fix dateText + parse function 2025-03-07 00:03:43 +07:00
Draken
288d6e0b8f Merge pull request #1543 - dragonx943/fixes
[NewTruyen] Small fixes
2025-03-05 23:37:47 +07:00
Draken
aa98db9ac6 [NewTruyen] Fixes 2025-03-05 16:34:06 +00:00
Draken
c19e1fd5e5 [Manganato] Update domain 2025-03-05 21:33:13 +07:00
Draken
ef10e3cafc [MangaNelo.com] Update domain 2025-03-05 21:31:12 +07:00
Draken
400c423dde [Mangakakalot.gg] Update domain 2025-03-05 21:30:03 +07:00
Draken
85ba4fa0de [Mangakakalot.gg] Update domain 2025-03-05 21:23:11 +07:00
Draken
8cb913d538 [TruyenHentai18] Update domain 2025-03-04 19:47:45 +07:00
Draken
2128bbe1b6 [TruyenHentai18] Fixes 2025-03-04 19:25:01 +07:00
Draken
4df07a50fb Merge pull request #1538 - dragonx943/sources/truyenhentai18
[TruyenHentai18] Add source
2025-03-04 18:05:00 +07:00
Draken
8fa63bca64 [TruyenHentai18] Add source 2025-03-04 10:58:49 +00:00
Draken
e191e0c9c1 Merge pull request #1529 - dragonx943/fixes
[Madara] Fixes + Update sources domain [vi]
2025-03-03 23:49:04 +07:00
Draken
5970509886 [site/vi] Update sources domain 2025-03-03 16:41:50 +00:00
Draken
b2c7d1028c [Madara] Fixes 2025-03-03 16:29:58 +00:00
Draken
377606d1aa [CBHentai + HentaiZ] Fixes 2025-03-03 16:21:13 +00:00
Draken
688bfdaa82 Merge pull request #1528 - dragonx943/sources/mangapill
[MangaPill] Add source
2025-03-03 22:36:03 +07:00
Draken
bc60629e66 [MangaPill] Add source 2025-03-03 22:29:58 +07:00
Draken
f3372751d1 [MangaPill] Add source 2025-03-03 22:29:35 +07:00
Draken
77a5216ebf Merge pull request #1524 - shub39/mangakakalot_fix
[Mangakakalot.com] -> [Mangakakalot.gg] Fixes
2025-03-03 20:26:30 +07:00
shub39
7ced91beea 🔧 no description 2025-03-03 12:24:34 +05:30
shub39
41f5cee6c6 ⚡ New way to get tags 2025-03-03 11:02:26 +05:30
shub39
ba5086c409 🔨 Mangakakalot fix
* issue #1522
* passing 5 out of 11 tests
2025-03-03 09:21:55 +05:30
531145c7f9 Change chapter name to title 2025-03-02 18:15:06 +02:00
f26fecb714 Small fixes 2025-03-02 18:15:06 +02:00
Draken
dd7568659f Fix warn 2025-03-02 19:34:23 +07:00
843d1f1bea Fixes 2025-03-01 16:38:25 +02:00
29263ff59b Add missing changes from #1496 2025-03-01 16:27:56 +02:00
3b91a3883e [Grouple] Fixes 2025-03-01 16:12:19 +02:00
ddb9b13df7 Some refactor and fix warnings 2025-02-28 14:50:36 +02:00
ed578e5bff Migrate altTitle to Set<String> 2025-02-27 19:08:38 +02:00
cdbb004ca1 Merge branch 'feature/search_query' 2025-02-27 17:46:14 +02:00
palaks-1
f6145bc412 Remove deprecated Manga constructor and introduce authors field 2025-02-27 17:17:53 +02:00
Draken
86e7c21e4d Merge pull request #1514 - VietAnh14/source/buondua
[BuonDua] Some fixes
2025-02-27 00:15:13 +07:00
ViAnh
76638c6aab [BuonDua] Some fixes 2025-02-26 23:40:09 +07:00
Draken
3a950f401b [BuonDua] Update domain 2025-02-26 22:45:12 +07:00
Draken
eeba47ea03 Merge pull request #1513 - VietAnh14/source/buondua
[BuonDua] New source
2025-02-26 22:41:47 +07:00
ViAnh
762f5cbec2 [BuonDua] New source 2025-02-26 22:35:41 +07:00
Draken
5b6c9be6d6 [CBHentai] Small fixes 2025-02-26 22:16:49 +07:00
f681ed270b Update parsers structure 2025-02-26 16:31:20 +02:00
a0168a7d49 Improvements 2025-02-26 15:25:26 +02:00
Draken
47263e641e Update Mangaoku.kt 2025-02-26 19:00:45 +07:00
Draken
70c78f1cab Update MangaKazani.kt 2025-02-26 19:00:45 +07:00
Draken
e7cb83f7b1 Update MangaSiginagi.kt 2025-02-26 19:00:45 +07:00
Draken
0212e1e18f Update PrunusScans.kt 2025-02-26 19:00:45 +07:00
Draken
265c411df2 Update RobinManga.kt 2025-02-26 19:00:45 +07:00
Draken
69a14b6fc5 Update YaoiTr.kt 2025-02-26 19:00:45 +07:00
Draken
168446e6d5 Update YaoiTr.kt 2025-02-26 19:00:45 +07:00
Draken
23af28f4dd Update NabiScans.kt 2025-02-26 19:00:45 +07:00
Draken
ac80384ba1 Update Mangaoku.kt 2025-02-26 19:00:45 +07:00
Draken
c4be0b58ca Update ImparatorManga.kt 2025-02-26 19:00:45 +07:00
Draken
023d13e44b Update EpikMan.kt 2025-02-26 19:00:45 +07:00
bai
f4909ade09 readded 2025-02-26 19:00:45 +07:00
bai
26fb2b9dcb new domain 2025-02-26 19:00:45 +07:00
bai
44c2ebd4a5 closed 2025-02-26 19:00:45 +07:00
bai
47874032a3 new domain 2025-02-26 19:00:45 +07:00
bai
6dfa5ad1c2 vlosed 2025-02-26 19:00:45 +07:00
bai
111ad58b57 closed 2025-02-26 19:00:45 +07:00
bai
f1086c93f7 change 2025-02-26 19:00:45 +07:00
bai
5e27a59414 change domain 2025-02-26 19:00:45 +07:00
bai
cece556669 change domain 2025-02-26 19:00:45 +07:00
bai
9252da8004 closed 2025-02-26 19:00:45 +07:00
bai
a9fdfee7ad mangagezgini new domain 2025-02-26 19:00:45 +07:00
bai
6904aecea3 they are closed 2025-02-26 19:00:45 +07:00
bai
b516632731 milasub new domain 2025-02-26 19:00:45 +07:00
bai
87ad5d0c3a nabiscans closed 2025-02-26 19:00:45 +07:00
bai
b95bb46d00 nyx mange closed 2025-02-26 19:00:45 +07:00
bai
60c91f9cc4 prunuscans closed 2025-02-26 19:00:45 +07:00
bai
20ce9cb8d9 robinmanga closed 2025-02-26 19:00:45 +07:00
bai
a2625410f9 rüyamange new domain 2025-02-26 19:00:45 +07:00
bai
e2697012ea viyafansub is closed 2025-02-26 19:00:45 +07:00
bai
f28cd3aad9 webtoonhatti new domain 2025-02-26 19:00:45 +07:00
bai
fd82a54d63 change to new domain 2025-02-26 19:00:45 +07:00
bai
d95d26a280 change site name 2025-02-26 19:00:45 +07:00
bai
33310bdc42 yaoi tr closed 2025-02-26 19:00:45 +07:00
bai
6281d770c1 zamanmanga is closed 2025-02-26 19:00:45 +07:00
Batu
c37f3bdad7 Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/tr/AlliedFansub.kt
allied fansub is closed
2025-02-26 19:00:45 +07:00
Draken
5630ca5982 Merge pull request #1507 - dragonx943/sources/pzt
[PointZero Toons] Add source
2025-02-26 00:03:07 +07:00
Draken
ceb75d98d0 [PointZero Toons] Add source
[PointZero Toons] Add source
2025-02-26 00:00:24 +07:00
Draken
75ad3b5657 [PointZero Toons] Add source
[PointZero Toons] Add source
2025-02-26 00:00:05 +07:00
Draken
5adf27eab1 Update SssScanlator.kt 2025-02-25 23:58:04 +07:00
Draken
18fbbed723 Merge pull request #1501 - palaks-1/source/nhentai_xxx_fix
[NHentai.xxx] Fix source
2025-02-25 09:40:30 +07:00
Draken
201bbf755d Merge pull request #1502 - AwkwardPeak7/weeb2
weebcentral: fix manga list & link resolving
2025-02-25 09:39:45 +07:00
AwkwardPeak7
b9fe35b8c3 weebcentral: fix manga list & link resolving 2025-02-25 07:10:02 +05:00
palaks-1
fd0787152e [NHentai.xxx] Fix source 2025-02-25 01:40:26 +02:00
29cf04c804 MangaParser interface 2025-02-24 17:59:40 +02:00
a4827d1b7d Merge branch 'palaks-1-new-search-query-proposal' into feature/search_query 2025-02-24 17:18:11 +02:00
f107e11528 Refactoring 2025-02-24 17:14:27 +02:00
Draken
52b6f75dd0 Merge pull request #1495 - dragonx943/sources/HentaiZ
[HentaiZ] Add source
2025-02-24 19:27:30 +07:00
Draken
fd719221ed [HentaiZ] Add source 2025-02-24 19:24:30 +07:00
Draken
a2c04fa954 [HentaiZ] Add source 2025-02-24 19:23:59 +07:00
Draken
6db5ac5f26 [HentaiVn.buzz] Small fixes 2025-02-24 19:07:01 +07:00
Draken
cedce23cf5 Merge pull request #1494 - sources/domain
- [TruyenQQ] Refactor
- [TruyenGG] Refactor
- Update some sources domain [site/vi]
2025-02-24 18:30:06 +07:00
Draken
645816f614 [site/vi] Update sources domain 2025-02-24 11:25:35 +00:00
Draken
00a769bfd5 Merge pull request #1492 - dragonx943/sources/HentaiVn
[HentaiVn.buzz] Add source
2025-02-24 17:45:49 +07:00
Draken
0fe5133992 Merge pull request #1493 - dragonx943/sources/SssScanlator
[sources/SssScanlator] Small fixes
2025-02-24 17:45:17 +07:00
Draken
2994f5936e Merge pull request #1491 - dragonx943/sources/cbhentai
[CBHentai] Fix tags
2025-02-24 17:42:47 +07:00
Draken
e8a96fa0ad [SssScanlator] Small fixes
Fix chapters > numChap [SssScanlator]
2025-02-24 17:30:38 +07:00
Draken
8c8cf5dcd9 [SssScanlator] Fix attempt 1 2025-02-24 10:11:20 +00:00
Draken
19ff5db70d [HentaiVn.buzz] Add source 2025-02-24 16:40:38 +07:00
Draken
c7b689842e [HentaiVn.buzz] Add source 2025-02-24 16:39:26 +07:00
Draken
df03c65d5a [CBHentai] Small fixes 2025-02-24 15:44:08 +07:00
Draken
4a88a93e8d [CBHentai] Fix tags
Fix "Root not found" / "Content not found or removed" error [CBHentai]
2025-02-24 13:29:34 +07:00
Draken
950b9e5510 [Vcomycs] Update source domain
Solved #1489
2025-02-24 00:10:25 +07:00
Draken
70230de8e1 [vi/KuroNeko] Fix source name
.../site/vi/viHentai.kt -> KuroNeko.kt [Việt Hentai]
2025-02-21 16:14:39 +07:00
palaks-1
c0ea9cadd7 New MangaSearchQuery implementation proposal 2025-02-19 00:19:21 +02:00
Draken
8c966c3e23 Merge pull request #1478 - VietAnh14/fixes
[Cmanga] Fixes
2025-02-19 00:56:52 +07:00
ViAnh
9f87e77ede [Cmanga] Fixes 2025-02-19 00:04:50 +07:00
ViAnh
6165c9b7cf [Cmanga] Update api 2025-02-19 00:02:18 +07:00
88ea5215c0 [MangaDex] Search by author 2025-02-18 18:12:13 +02:00
522b3d53cc Artist filter support 2025-02-18 17:48:20 +02:00
Draken
9a1d37166a [CManga] Small fixes
[CManga] Fix "Nothing found" bug #1477
2025-02-18 13:04:23 +07:00
Draken
07ae87f72b [Kiryuu] Update domain
[Kiryuu] Fix looping captcha
2025-02-17 21:40:33 +07:00
Draken
a16a202800 Merge PR #1475 - dragonx943/sources/viHentai
[viHentai] Add source
2025-02-17 19:42:40 +07:00
Draken
09ff9c0a7d [viHentai] Fix attempt 2 2025-02-17 12:16:47 +00:00
Draken
8c4a93d1dc [viHentai] Fix attempt 1 2025-02-17 11:57:03 +00:00
Draken
2da8a4af84 [viHentai] Add source 2025-02-17 11:19:23 +00:00
Draken
7d5f0d3187 Merge pull request #1464 - scryptan/wamanga-fix
[WaManga] Fixes
2025-02-16 11:28:10 +07:00
Draken
a65c1810a2 Merge pull request #1465 - palaks-1/sources/nhentai_xxx
[NHentai.xxx] Fix source
2025-02-16 11:25:18 +07:00
palaks-1
09b7d27b40 [NHentai.xxx] Fix source 2025-02-16 04:16:01 +02:00
scryptan
e1175d01cc use api, instead of html parse 2025-02-16 06:41:45 +05:00
Draken
51fe77ff33 Merge pull request #1461 - dragonx943/site/vi/newtruyen
[NewTruyen] Add source + Update sources domain [vi]
2025-02-16 01:18:19 +07:00
Draken
c540266e48 [NewTruyen] Add source 2025-02-15 18:10:45 +00:00
Draken
198e859850 [HamTruyen] Small fixes
[HamTruyen] Fix chapters order + Number
2025-02-14 22:42:01 +07:00
Draken
87944719a5 Merge pull request #1457 - palaks-1/sources/nhentai_xxx
[NHentai.xxx] Add source
2025-02-13 23:42:22 +07:00
palaks-1
6b44656819 [NHentai.xxx] Add source 2025-02-13 17:50:10 +02:00
Draken
35334e82ae Merge pull request #1456 - palaks-1/sources/nhentai_to
[NHentai.to] Add source
2025-02-13 19:55:28 +07:00
palaks-1
32e7b7184f [NHentai.to] Add source 2025-02-13 14:50:08 +02:00
Draken
4098183be7 Merge pull request #1453 - dragonx943/sources/muitohentai
[MuitoHentai] Fix "Content not found or removed" errors
2025-02-13 01:14:52 +07:00
Draken
e25b03b68f [MuitoHentai] Fix attempt 3 2025-02-12 17:49:27 +00:00
Draken
7076b2d443 [MuitoHentai] Fix attempt 2 2025-02-12 17:26:45 +00:00
Draken
1933200372 [MuitoHentai] Fixes 2025-02-12 17:04:45 +00:00
Draken
0359afe574 Merge pull request #1452 - dragonx943/sources/hamtruyen
[HamTruyen] Add source
2025-02-12 13:19:52 +07:00
Draken
cd8282bbe0 [HamTruyen] Add source 2025-02-12 06:14:06 +00:00
Draken
8350b3c70e Merge branch 'sources/hamtruyen' of https://github.com/dragonx943/Kotatsu-parsers into sources/hamtruyen 2025-02-12 05:57:12 +00:00
Draken
8488d80c6f Merge branch 'KotatsuApp:master' into sources/hamtruyen 2025-02-12 12:56:52 +07:00
Draken
affd4646af Merge pull request #1451 - dragonx943/fixes
[Hentai18VN, TruyenHentaiVN] Refactor + Fixes
2025-02-12 12:54:10 +07:00
Draken
9853638c6b [Hentai18VN] Refactor 2025-02-12 05:49:08 +00:00
Draken
faf2f2a312 [HentaiVN18] Refactor + Fixes 2025-02-12 05:24:22 +00:00
Draken
4adf485208 Merge branch 'fixes' of https://github.com/dragonx943/Kotatsu-parsers into fixes 2025-02-12 04:24:38 +00:00
Draken
eaab76aa9e [HamTruyen] Add source 2025-02-11 18:38:42 +00:00
Draken
2ca740a85d Merge pull request #1450 - dragonx943/sources/hentai18vn
[Hentai18VN] Add source
2025-02-12 01:10:18 +07:00
Draken
569aa73cd4 [Hentai18VN] Fix attempt 3 2025-02-11 18:00:14 +00:00
Draken
d3858ff5ff [Hentai18VN] Fix attempt 2 2025-02-11 17:41:18 +00:00
Draken
2760dc8a76 [Hentai18VN] Fix attempt 1 2025-02-11 17:23:30 +00:00
Draken
c1990dde75 Merge branch 'sources/hentai18vn' of https://github.com/dragonx943/Kotatsu-parsers into sources/hentai18vn 2025-02-11 16:50:41 +00:00
Draken
af5b62b8fd Merge branch 'KotatsuApp:master' into sources/hentai18vn 2025-02-11 23:09:13 +07:00
Draken
fcd6e92dba [Hentai18VN] Add source 2025-02-11 16:08:46 +00:00
Draken
dad098ff34 Merge pull request #1449 from dragonx943/fixes
[TruyenHentaiVN] Fix tags
2025-02-11 20:57:27 +07:00
Draken
aa9a499a09 [TruyenHentaiVN] Fix tags 2025-02-11 13:30:27 +00:00
Draken
f10e6f021c Add base code for Hentai18VN 2025-02-11 13:25:33 +00:00
Draken
09596331fd [TruyenHentaiVN] Fix tags 2025-02-11 20:21:47 +07:00
Draken
c8f9962def [TruyenHentaiVN] Fix tags 2025-02-11 20:16:39 +07:00
Draken
e91e8fcccb Merge branch 'sources/hentai18vn' of https://github.com/dragonx943/Kotatsu-parsers into sources/hentai18vn 2025-02-11 12:58:34 +00:00
Draken
a702f1f7ef Merge pull request #1439 from scryptan/wamanga-fix
[WaManga] Fix NoSuchMethodError
2025-02-11 19:20:18 +07:00
Draken
9654a68dad Merge pull request #1443 - VietAnh14/fixes
[CManga] Fixes
2025-02-10 21:13:48 +07:00
ViAnh
a2f18e2284 [Cmanga] Remove escape chars in description 2025-02-10 21:11:23 +07:00
ViAnh
fceb927bce [Cmanga] Small fixes 2025-02-10 20:44:29 +07:00
ViAnh
f192c1716d [Cmanga] Filter unknown manga item 2025-02-10 20:31:29 +07:00
Draken
f2960c2229 Merge pull request #1442 - dragonx943/sources/manhwaindo
[ManhwaIndo] Fix "Content not found or removed"
2025-02-10 15:29:20 +07:00
Draken
552a68c1d4 [ManhwaIndo] Fix attempt 1 2025-02-10 08:14:29 +00:00
Draken
e5f277c957 Merge pull request #1441 from AwkwardPeak7/weeb
WeebCentral: some fixes
2025-02-10 12:08:03 +07:00
AwkwardPeak7
ff05dd7694 [WeebCentral] fix chapter number not correct in some cases 2025-02-10 08:24:23 +05:00
AwkwardPeak7
7c604647bf [WeebCentral] fix tags not loading 2025-02-10 08:23:39 +05:00
Draken
0fd0c38971 Merge pull request #1440 - dragonx943/sources/truyenhentaivn
Add new source: TruyenHentaiVN
2025-02-10 03:26:12 +07:00
Draken
a454613ce8 [TruyenHentaiVN] Add source 2025-02-09 20:22:35 +00:00
Draken
73dd8c4688 [TruyenHentaiVN] Add source 2025-02-09 20:21:28 +00:00
scryptan
1a07cfc144 try fix not exist optJSONObject in kotatsu app 2025-02-09 19:41:27 +05:00
Draken
715fe9e645 [Weeb Central] Add source 2025-02-09 21:14:05 +07:00
Draken
6316ac055c Merge pull request #1438 from AwkwardPeak7/weeb
[Weeb Central] Add source
2025-02-09 21:13:22 +07:00
Draken
ff64db7a7c Merge pull request #1437 from VietAnh14/fixes
[NetTruyen] Fix fetch chapters
2025-02-09 21:11:37 +07:00
AwkwardPeak7
d5a299d7ff mark MangaSee and MangaLife as broken 2025-02-09 19:05:11 +05:00
AwkwardPeak7
0e3c3f89d0 add Weeb Central 2025-02-09 19:04:54 +05:00
ViAnh
234e23b7a5 [NetTruyenVie] Fix fetch chapters 2025-02-09 20:47:33 +07:00
ViAnh
69093b8214 [NetTruyen] Fix fetch chapters 2025-02-09 20:18:09 +07:00
Draken
708d7908aa [GocTruyenTranh] Fixes
Add NSFW detector by Tags [site/vi/GocTruyenTranh]
2025-02-09 19:30:44 +07:00
Draken
b08051789d [GocTruyenTranh] Fixes
Fix author + "null" in altTitle [GocTruyenTranh]
2025-02-09 16:22:59 +07:00
Draken
794a737b6d Merge pull request #1435 - dragonx943/vi/goctruyentranh
Add new source: GocTruyenTranh
2025-02-09 04:04:41 +07:00
Draken
b1ef29fadf [GocTruyenTranh] Add source 2025-02-08 20:58:10 +00:00
Draken
0475fc81ed [GocTruyenTranh] Add source 2025-02-08 20:57:04 +00:00
6aefb603ae [NetTruyen] Fix chapters order 2025-02-08 16:40:46 +02:00
Draken
92bdefcf9d Merge pull request #1432 - dragonx943/sources/wpcomics
[site/wpcomics/vi] Fixes
2025-02-07 19:58:27 +07:00
Draken
576de54ef8 [wpcomics/vi/nettruyen*] Fix fetch invaild format 2025-02-07 12:15:09 +00:00
Draken
319225d645 [wpcomics/vi/nettruyen*] Fixes 2025-02-07 04:53:33 +00:00
Draken
9addea65d6 [wpcomics/vi/nettruyen*] Fixes 2025-02-07 04:51:41 +00:00
Draken
95c43a06d5 [wpcomics/vi/nettruyen*] Fixes 2025-02-07 04:23:38 +00:00
Draken
72c7317672 Merge pull request #1429 - dragonx943:sources/nsfw
Mark some sites as NSFW sources
2025-02-05 22:38:22 +07:00
Draken
1cec3e5436 Mark some sites as NSFW sources 2025-02-05 15:31:47 +00:00
Draken
6c18056cef [NetTruyenVie] Fix attempt 1 2025-02-04 02:19:35 +07:00
Draken
f4b678083d Merge pull request #1422 from VietAnh14/source/vivicomi
[Vcomycs] New source
2025-02-03 16:11:36 +07:00
ViAnh
50eb6f7b7e [Vcomycs] Update content type 2025-02-03 15:59:46 +07:00
ViAnh
0b6af2549f [Vcomycs] New source 2025-02-03 15:53:47 +07:00
Draken
f229d95469 Merge pull request #1421 - dragonx943:site/domain
Update some sources domain
2025-02-02 23:59:18 +07:00
hoanphonglinh
7ba725de6d [vi] Update sources domain 2025-02-02 16:52:24 +00:00
Draken
677fd51e91 Update ManhuaScan.kt 2025-02-02 15:32:09 +07:00
45b843fadc Replace try-catch with use{} in parse functions 2025-02-02 10:14:48 +02:00
9ec62f0be0 Refactor 2025-02-02 10:07:31 +02:00
Draken
9baba17692 Merge pull request #1413 from dragonx943/sources/manhwaindo
[Manhwaindo] Fix missing images
2025-01-31 23:54:07 +07:00
hoanphonglinh
d86d0a5d5b [Manhwaindo] Fix attempt 2 2025-01-31 16:27:41 +00:00
hoanphonglinh
543e8e3267 [Manhwaindo] Fix attempt 1 2025-01-31 16:06:56 +00:00
Draken
267dcf8fa7 [TruyenTranh3Q] Fixes 2025-01-30 23:09:10 +07:00
Draken
fec4594480 Merge pull request #1410 from dragonx943:site/vi/final
[TruyenTranh3Q] Add source
2025-01-30 22:55:50 +07:00
hoanphonglinh
f669df2ff5 [TruyenTranh3Q] Add source 2025-01-30 15:47:40 +00:00
hoanphonglinh
9dfa652125 [TruyenTranh3Q] Add source 2025-01-30 15:47:04 +00:00
Draken
87f1cd8cdc Update Komiku.kt 2025-01-29 22:29:15 +07:00
Draken
f30f8ee563 Merge pull request #1408 from dragonx943:site/vi/yurineko
[YuriNeko] Fix 403 errors + Missing images
2025-01-29 18:15:15 +07:00
Draken
64eceba9fa [YuriNeko] Fix domain 2025-01-29 18:09:00 +07:00
Draken
58e09bdaba Merge pull request #1397 from sources/nettruyenvie
[NetTruyenVie] Add source
2025-01-24 22:08:51 +07:00
Koitharu
d4e9040ccf Fixes 2025-01-24 14:17:47 +00:00
Koitharu
653894cb82 First commit from local/Kotatsu-parsers 2025-01-24 14:08:57 +00:00
Draken
581cb0c1fe First commit from local/Kotatsu-parsers 2025-01-24 14:07:27 +00:00
4e545ca7af [WaManga] Refactor #1390 2025-01-23 20:54:26 +02:00
scryptan
b4050e0335 refactor 2025-01-23 20:44:12 +02:00
scryptan
1cecd1fe94 add wamanga parser 2025-01-23 20:44:12 +02:00
51ed1b2db8 Update json utils 2025-01-22 09:34:27 +02:00
Draken
3e4d712dca Merge pull request #1385 from dragonx943/sources/domain
Update sources domain
2025-01-19 21:37:12 +07:00
Draken
d284b647ab Merge branch 'KotatsuApp:master' into sources/domain 2025-01-19 21:29:13 +07:00
Draken
3bcd05fc5f Update sources domain 2025-01-19 14:27:03 +00:00
b0a1cc48a6 Update utils 2025-01-19 11:07:48 +02:00
Draken
2eba38b289 Update domains 2025-01-18 03:24:03 +00:00
8481fadbd0 Fix absolute url resolving 2025-01-16 08:51:55 +02:00
6abcdd8d4b Fix nullable strings usage 2025-01-12 19:16:46 +02:00
5df1445e29 Migrate models to data class and update Manga class 2025-01-12 18:16:30 +02:00
Draken
a94adf4d90 [CuuTruyen] Fixes
Fix timer + Add manga status for CuuTruyen
2025-01-03 17:49:38 +07:00
dragonx943
f21aa282dd [CuuTruyen] Fix attempt 1 2025-01-03 08:44:13 +00:00
Draken
8ce6694232 Merge pull request #1336 from dragonx943/sources/vi
[sources/vi] Fix sources domain
2024-12-31 20:47:16 +07:00
Draken
d8bd51e17e [site/vi] Change sources domain 2024-12-31 13:40:01 +00:00
Draken
b1b04d2953 Merge pull request #1331 from dragonx943/final
Update domain + Fixes
2024-12-28 16:01:49 +07:00
dragonx943
8852020ca8 [MeHentaiVN] Add source 2024-12-28 08:55:16 +00:00
Draken
deb4f763d0 Merge pull request #2 from dragonx943/test
[NetTruyen] Fixes
2024-12-28 11:13:21 +07:00
dragonx943
58593de53d [NetTruyen] Fix attempt 2 2024-12-28 03:44:29 +00:00
dragonx943
040be87a54 [NetTruyen] Fix attempt 1 2024-12-28 02:27:22 +00:00
Draken
1703ac39d5 Rename + Change domain sources from dragonx943/test
Rename + Change domain sources
2024-12-27 18:16:53 +07:00
dragonx943
839869061e Rename some sources 2024-12-27 11:14:33 +00:00
764c65563b [Grouple] Fixes 2024-12-23 11:02:41 +02:00
2550b9cac1 [KomikTap] Fix http pages 2024-12-19 17:00:16 +02:00
04225170d3 [Grouple] Fix pages parsing 2024-12-19 16:57:19 +02:00
326a3f78b2 Fix Response.mimeType extension 2024-12-19 14:58:40 +02:00
f86d31f811 Update utils 2024-12-18 14:00:43 +02:00
vianh
10dac6c0d4 [CManga] New source 2024-12-15 10:07:46 +02:00
devi
fece09b781 Merge remote-tracking branch 'origin/master' 2024-12-13 23:57:51 +01:00
devi
6b1fb90584 Update source
[AnimeH] Broken source close #1301
 [SussyScan] fix close #1298
 close #1287
 close #1282
 close #1273
 close #1267
 close #1268
 close #1260
 close #1234
 [Manhwa18] fix src close #1297
 close #1290
 close #1288
[KomikCast] close #1284
close #1283
close #1272
close #1270
close #1265
[Birdtoon] close #1276
[Komiktap] close #1275
[SadScans] close #1274
[Flamecomics] reverse chapter fix close #1266
[Kiryuu] fix url close #1258
[MangaTime] Broken close #1228
[Dexhentai] fix close #1226
2024-12-13 23:57:09 +01:00
5055cfddbd Merge pull request #1294 from dragonx943/patch-1 2024-12-10 18:19:38 +02:00
Draken
878d20e612 [ComicExtra] Remake #1 2024-12-10 23:02:36 +07:00
336f64712d Merge pull request #1292 from dragonx943/patch-1 2024-12-09 17:08:40 +02:00
Draken
56989c6b2f Update Fecomicc.kt 2024-12-09 20:59:27 +07:00
Draken
7b1d3dcd46 Fixes 2024-12-09 20:32:12 +07:00
f1e66daf06 Merge pull request #1291 from dragonx943/test 2024-12-09 12:35:00 +02:00
dragonx943
ca087290b9 [YuriNeko] Fix attempt 1 2024-12-09 09:49:31 +00:00
883886bc32 Merge pull request #1281 from dragonx943/test 2024-12-05 19:12:18 +02:00
Draken
9e60253eb1 Merge branch 'KotatsuApp:master' into test 2024-12-05 23:53:54 +07:00
dragonx943
dba85f2bff Fixes 2024-12-05 16:52:14 +00:00
e5b852793e Merge pull request #1280 from dragonx943/test 2024-12-05 18:43:38 +02:00
Draken
d4cbd86b96 Update summary.yaml 2024-12-05 23:37:23 +07:00
Draken
8d82bacaa3 Merge branch 'KotatsuApp:master' into test 2024-12-05 23:35:33 +07:00
dragonx943
4af760c172 Fixes 2024-12-05 16:35:02 +00:00
dragonx943
7747bc53b1 Update domain + Refactor 2024-12-05 16:29:22 +00:00
0456a92f15 [WpComics] Fix covers 2024-12-05 18:23:03 +02:00
Draken
677afa9a41 Update TopTruyen.kt (#1279) 2024-12-05 17:00:40 +02:00
ebcce4f2ec [DocTruyen3Q] Fix covers 2024-12-05 15:34:39 +02:00
91e53e4872 [AsuraScans] Fix pages parsing 2024-11-27 11:56:42 +02:00
8bc51b3b79 [FlameComics] Rewrite #1237 2024-11-23 15:02:27 +02:00
733d3ca69f [ResetScans] Migrate to the new website (close #1252) 2024-11-23 15:01:51 +02:00
4a2e465e5d [NineManga] Fixes #1253 2024-11-23 15:01:51 +02:00
Draken
7ed3c5a175 Update sources domain (#1254)
* Update Mangarbic.kt

* Update BlogTruyenParser.kt

* Update CuuTruyenParser.kt

* Update HentaiVNParser.kt

* Update LxManga.kt

* Update DocTruyen3Q.kt

* Update NetTruyenHE.kt

* Update NetTruyenLL.kt

* Update NetTruyenSSR.kt

* Update NetTruyenUU.kt

* Update NhatTruyenVN.kt

* Update TopTruyen.kt

* Update OtakusanVi.kt

* Update OtakusanEn.kt

* Update HentaiVnPlus.kt

* Update TruyenTranhDamMyy.kt

* Update TruyenVn.kt

* Update DocTruyen5s.kt
2024-11-23 13:59:25 +02:00
bivaly
8567cedf0a Updating FlameComics ListUrl (#1255)
* Update FlameComics.kt

* Update FlameComics.kt

* Update FlameComics's listUrl

Ther are no longer a listUrl but maybe they are  going to revert the changes so leaving there the overide for easy changing

* removing FlameComics ListUrl

Ther are no longer a listUrl but maybe they are  going to revert the changes so leaving there the overide for easy changing
2024-11-23 13:58:48 +02:00
e02b563bf4 [AsuraComic] Fix pages parsing 2024-11-21 09:15:12 +02:00
Draken
27d2814ef9 [LectorManga] Update domain (#1233)
* Update LectorManga.kt

* Update LikeManga.kt
2024-11-21 07:57:13 +02:00
Draken
de95fa2e3e Update domain + Add sources (#1221)
* Update HariManga.kt

* Update Mgkomik.kt

* Update XoxoComics.kt

* Create DuaLeoTruyen.kt

* Create MyComicList.kt

* Update DuaLeoTruyen.kt

* Update summary.yaml

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Add suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Fixes

---------

Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-11-16 20:31:18 +02:00
5a4f9d8914 [MangaFire] Fix search 2024-11-16 16:42:31 +02:00
23271c4623 [LibSocial] Use site domains 2024-11-16 10:25:23 +02:00
f3d14e101c [MintManga] Update domain 2024-11-10 14:02:34 +02:00
275d7f5419 [ImHentai] Fix pages parsing 2024-11-10 12:52:48 +02:00
ac163085e1 [ImHentai] Fix pages parsing 2024-11-10 12:07:58 +02:00
Draken
8b4bac3cc2 [TruyenGG] Refactor (#1215)
* Update TruyenGG.kt

* Update TruyenGG.kt
2024-11-09 20:19:10 +02:00
35f4db7905 Rollback kotlin to fix build 2024-11-09 20:12:05 +02:00
08b1241a68 Remove redundant trim and mapNotNullToSet usage 2024-11-09 19:43:23 +02:00
3ffcefaa1b Get rid of redundant boxing 2024-11-09 19:11:41 +02:00
3b173dc6fc Improve utils 2024-11-09 19:04:22 +02:00
Draken
60f1fb1f70 Update + Add source (#1213)
* Update YurinekoParser.kt

* Create TruyenGG.kt

* Update and rename Truyenqq.kt to TruyenQQ.kt

* Update TruyenGG.kt

* Update summary.yaml

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Fix tags

---------

Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-11-09 11:53:26 +02:00
devi
f610ae6412 [ManhwaLatino] fix the order of the chapters and some manga don't display all the chapters.
close #1211
2024-11-05 20:19:30 +01:00
f80b586081 [Iken] Fixes 2024-11-04 10:45:55 +02:00
7999064dd4 [MangaFire] Fix search (close #1196) 2024-11-04 10:41:54 +02:00
8c387e7983 [Desu] Cleanup 2024-11-04 10:33:48 +02:00
1d29cacf04 [LxManga] Refactor 2024-11-04 10:27:22 +02:00
Draken
ce4b4d06d0 Update domain + Fixes (#1206)
* Update MaidScan.kt

* Update NetTruyenLL.kt

* Update Quaanhdaocuteo.kt

* Fixes
2024-11-04 10:09:24 +02:00
79e1d59482 Fixes 2024-11-01 12:44:09 +02:00
devi
47e1c0fa89 Minor changes 2024-10-31 21:45:09 +01:00
devi
3ec23a56ab Minor changes 2024-10-31 21:44:27 +01:00
devi
a01493e071 [EpsilonScan] fix close #1038
[Keyoapp] fix pages close #1192
[Yaoiflix] Fix close #1201
[Teamxnovel] fix Searching close #1176
close #1185
[ManhwaLatino] close #1181
[MiHentai] fix close #1180
[GenzToon] close #1169
[MangaGalaxy] close #1168 ( redirect to VortexScans )
[CatharsisFantasy] close #1158
[CatharsisWorld] close #1157
Update soures
2024-10-31 21:41:11 +01:00
Draken
16052210c1 [NhatTruyenVN] Fixes (#1199)
* Update NhatTruyenVN.kt

* Update src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenVN.kt

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

* Update suggestions

Co-authored-by: Koitharu <nvasya95@gmail.com>

---------

Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-10-31 13:34:56 +02:00
7dd9658864 [Ahen] Fix chapters parsing 2024-10-30 14:02:13 +02:00
f794f411b7 [DesuMe] Fix covers 2024-10-29 12:44:23 +02:00
Draken
94fbbad6f3 [DocTruyen3Q + TopTruyen] Fixes (#1187)
* Update DocTruyen3Q.kt

* Update CuuTruyenParser.kt

* Update NetTruyen.kt

* Update DocTruyen3Q.kt

* TopTruyen sus :p

* Update TopTruyen.kt

* Remove unavailable tags

* Update TopTruyen.kt

* Remove redundant trim() calls

* Update DocTruyen3Q.kt

* Update TopTruyen.kt

* Remove some unavailable tags

* Update TopTruyen.kt

* Update DocTruyen3Q.kt

* Update TopTruyen.kt

---------

Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-10-27 12:03:23 +02:00
4c5ed57958 Fix json iterator 2024-10-26 08:34:49 +03:00
Draken
3d5cc5ceff Update TruyenVn.kt 2024-10-21 10:04:13 +03:00
Draken
c506153737 Update OtakusanEn.kt 2024-10-21 10:04:13 +03:00
Draken
95ee8a4a16 Update OtakusanVi.kt 2024-10-21 10:04:13 +03:00
Draken
3f7b0695f4 Update LxManga.kt 2024-10-21 10:04:13 +03:00
Draken
b3bb0939de Update HentaiVnPlus.kt 2024-10-21 10:04:13 +03:00
08fe54c36d Refactor json utilities 2024-10-18 08:58:16 +03:00
166c5be5d6 Refactoring 2024-10-17 18:24:11 +03:00
7ce2a97c1f [DocTruyen5s] Refactoring 2024-10-17 17:42:17 +03:00
Draken
b23e241973 Update DocTruyen5s.kt 2024-10-17 17:12:39 +03:00
Draken
b8b54b7139 Update DocTruyen5s.kt 2024-10-17 17:12:39 +03:00
Draken
5853e54684 Update SayHentai.kt 2024-10-17 17:12:39 +03:00
Draken
32e3b07e5d Update HentaiCube.kt 2024-10-17 17:12:39 +03:00
Draken
3b99676e7a Update HentaiCube.kt 2024-10-17 17:12:39 +03:00
Draken
3c829302c7 Update TopTruyenViet.kt 2024-10-17 17:12:39 +03:00
Draken
b579b01db5 Update DocTruyen3Q.kt 2024-10-17 17:12:39 +03:00
Draken
7db798c420 Update CuuTruyenParser.kt 2024-10-17 17:12:39 +03:00
Draken
77ed11b58e Update summary.yaml 2024-10-17 17:12:39 +03:00
Draken
3ae6ffb8d0 Tweak :p 2024-10-17 17:12:39 +03:00
Draken
0e5e4d57c5 Create SayHentai.kt 2024-10-17 17:12:39 +03:00
d8cb38a9be Improve links resolving 2024-10-16 12:36:31 +03:00
07b0a2da9e Fix loading manga by link 2024-10-16 12:36:31 +03:00
1d040e8291 Link resolver implementation 2024-10-16 12:36:27 +03:00
davvarrr
797a91a037 Merge pull request #1167 from dragonx943/patch-1
Daily update
2024-10-14 21:39:32 +02:00
devi
9c0b57835f [WebtoonEmpire] fix close #1134 2024-10-14 21:38:25 +02:00
devi
72564f5449 [MadaraParser] auto fix url manga on
fix query ( withoutAjax
[MangaPark] fix close #1152
close #1145
[AdultWebtoon] fix close #1139
[ComicExtra] change type close #1130
[liliana] fix and add ManhuaPlusorg, Raw1001, MangaSect, MangaKoma01, ManhuaGold
2024-10-14 21:09:35 +02:00
Draken
cc18692538 Update ComickFunParser.kt 2024-10-15 00:10:19 +07:00
Draken
3e102e9d69 Update BlogTruyenParser.kt 2024-10-14 20:04:33 +03:00
Draken
b9b53bcc91 Add some tags 2024-10-14 20:04:33 +03:00
Draken
00ef8e5cdf Remove broken tag 2024-10-14 20:04:33 +03:00
Draken
a738591dd0 Update DocTruyen5s.kt 2024-10-14 20:04:33 +03:00
Draken
85771bcd89 Fix BlogTruyen 2024-10-14 20:04:33 +03:00
0150ede4cb [Liliana] Fix chapters order 2024-10-13 15:26:58 +03:00
Draken
1406165789 Update LxManga.kt 2024-10-13 15:11:40 +03:00
Draken
a62eadb581 Fix filters 2024-10-13 15:11:40 +03:00
Draken
197b148fca Update Hijala.kt 2024-10-11 09:51:18 +03:00
Draken
fd4a1cda2d Update HentaiCube.kt 2024-10-11 09:51:18 +03:00
Draken
108920c4fd Update DocTruyen5s.kt 2024-10-11 09:51:18 +03:00
Draken
49c7702707 Update LilianaParser.kt 2024-10-11 09:51:18 +03:00
Draken
15361654b7 Create LilianaParser.kt 2024-10-11 09:51:18 +03:00
Draken
c2b64d40d1 Create DocTruyen5s.kt 2024-10-11 09:51:18 +03:00
ce9404da84 [Madara] Fix multiple pages handling 2024-10-09 17:18:48 +03:00
5c55d65eb3 Refactor and fixes 2024-10-09 08:18:29 +03:00
Draken
6f9180545b Update and rename HentaiVnParser.kt to HentaiVnPlus.kt 2024-10-09 08:07:05 +03:00
Draken
e966aa07cd Create BlogTruyenParser.kt 2024-10-09 08:07:05 +03:00
Draken
102993cb10 Remove BlogTruyen - Closed site 2024-10-09 08:07:05 +03:00
Draken
d0627b83d0 Update BlogTruyenVN.kt 2024-10-09 08:07:05 +03:00
Draken
a6602a0f5a Update BlogTruyenVN.kt 2024-10-09 08:07:05 +03:00
Draken
55b9a72795 Create BlogTruyenVN.kt 2024-10-09 08:07:05 +03:00
Draken
d4f0c9d116 Update LxManga.kt 2024-10-09 08:07:05 +03:00
Draken
bbb3184454 Update CuuTruyenParser.kt 2024-10-09 08:07:05 +03:00
Draken
7d81e68757 Update CuuTruyenParser.kt 2024-10-09 08:07:05 +03:00
Draken
487388c950 Update DocTruyen3Q.kt 2024-10-09 08:07:05 +03:00
Draken
9d507eb10c Update TopTruyenViet.kt 2024-10-09 08:07:05 +03:00
Draken
cf9b19ffa7 This source will no longer work!
This site has been sold and it has changed name + source structure :p

Changed to: https://github.com/KotatsuApp/kotatsu-parsers/issues/1135
2024-10-09 08:07:05 +03:00
Draken
9591269a68 Update TopTruyenViet.kt 2024-10-09 08:07:05 +03:00
Draken
27fe426213 Update DocTruyen3Q.kt 2024-10-09 08:07:05 +03:00
Draken
b23d899f5e Update CuuTruyenParser.kt 2024-10-09 08:07:05 +03:00
Draken
8bf0c03fe2 Tags ? 2024-10-09 08:07:05 +03:00
Draken
1ebb298cd7 Update CuuTruyenParser.kt 2024-10-07 15:16:49 +03:00
Draken
9f588ab9fd Rename HentaiVn.kt to HentaiVnParser.kt 2024-10-07 15:16:49 +03:00
Draken
91a1198900 Update HentaiVn.kt 2024-10-07 15:16:49 +03:00
Draken
321ac087dc Update TruyenVn.kt 2024-10-07 15:16:49 +03:00
Draken
5d81c030b5 Rename HentaiVnFit.kt to HentaiVn.kt 2024-10-07 15:16:49 +03:00
Draken
e798bc7787 Update HentaiVnFit.kt 2024-10-07 15:16:49 +03:00
Draken
057b053d58 Update TopTruyenViet.kt 2024-10-07 15:16:49 +03:00
Draken
e9a9ef6b95 Update DocTruyen3Q.kt 2024-10-07 15:16:49 +03:00
Draken
bd7062a479 Update LxManga.kt 2024-10-07 15:16:49 +03:00
a659068ef3 Fixes batch 2024-10-07 15:15:52 +03:00
6f7e1fcfb2 [Grouple] Fix closing response on error 2024-10-05 15:23:22 +03:00
Draken
eff26c8889 Create NetTruyenFE.kt 2024-10-05 13:40:59 +03:00
Draken
037059d0e6 Update NetTruyenHE.kt 2024-10-05 13:40:59 +03:00
Draken
9b78a3ecf2 Update PinkTeaComic.kt 2024-10-05 13:40:59 +03:00
Draken
4df8311e18 Update Truyenqq.kt 2024-10-05 13:40:59 +03:00
645006fde8 Fix closing response in interceptors 2024-10-04 08:28:48 +03:00
95bbfefe90 Merge pull request #1128 from dragonx943/patch-1
Daily update sources
2024-10-03 09:17:28 +03:00
Draken
3181a5860b Update Truyenqq.kt 2024-10-03 10:48:22 +07:00
Draken
39c83444b7 Broken:( 2024-10-03 10:43:17 +07:00
Draken
7356d2036d Update BlogTruyenVNParser.kt 2024-10-03 10:40:30 +07:00
a8df8665ae [CuuTruyen] Fix tags 2024-10-02 11:02:46 +03:00
6355252699 Merge pull request #1117 from dragonx943/het-cuu-truyen
[CuuTruyen] Fix tags
2024-10-02 10:49:53 +03:00
Draken
6ec65e4aa4 Update TopTruyenViet.kt 2024-10-01 13:08:23 +07:00
Draken
f58bb421e5 Update DocTruyen3Q.kt 2024-10-01 13:08:05 +07:00
Draken
38ecde185a Update CuuTruyenParser.kt 2024-09-30 02:17:42 +07:00
Draken
e3924d5b06 Remove something
Remove some Tags (not found manga with it)
2024-09-30 02:10:08 +07:00
Draken
c1a7fc2624 Update CuuTruyenParser.kt 2024-09-30 01:29:46 +07:00
818d14fdbb Merge pull request #1109 from dragonx943/patch-2
Update domain
2024-09-29 19:25:01 +03:00
Draken
e93a6865e7 Update HentaiVNParser.kt 2024-09-28 20:07:57 +07:00
Draken
c6325eef50 Update DocTruyen3Q.kt 2024-09-28 18:59:16 +07:00
Draken
06d6653c78 Create DocTruyen3Q.kt 2024-09-28 18:54:05 +07:00
Draken
aac722909b Update HentaiVNParser.kt 2024-09-28 18:42:20 +07:00
Draken
78d64713a2 Create NetTruyenUU.kt 2024-09-28 18:36:10 +07:00
devi
7d71421c30 Update sources
Close #1105
Close #1104
2024-09-27 17:31:07 +02:00
7a571e596d [NHentai] Fix duplicated preferences 2024-09-27 08:00:01 +03:00
davvarrr
1718a034ee Merge pull request #1101 from dragonx943/patch-1
Update domain (Fetch data error)
2024-09-26 23:51:30 +02:00
devi
5443daabb6 [PornComix] fix close #1100 2024-09-26 23:50:53 +02:00
devi
de4f8ef2f9 [madtheme] add SearchWithFilters
[manga18] add SearchWithFilters
[mangadventure] add SearchWithFilters
[mangaworld] add POPULARITY_ASC, NEWEST_ASC, SearchWithFilters, Year, ContentTypes, Multiple states
Remove type on fetch tags
[mmrcms] add SearchWithFilters
[TrWebtoon] add SearchWithFilters
[Truyenqq] add TagsExclusion, ContentTypes, NEWEST_ASC, UPDATED_ASC, POPULARITY_ASC
[Baozimh] add ContentTypes
[zmanga] add SearchWithFilters, Year, ContentTypes
2024-09-26 23:26:07 +02:00
Draken
b9fb4a5e93 Update HentaiVNParser.kt 2024-09-26 22:35:01 +07:00
3cdd391410 [ExHentai] Use ContentType filter 2024-09-26 13:53:10 +03:00
ad77e8afa4 [CuuTruyen] Fix UserAgent 2024-09-26 13:41:04 +03:00
c85d17d60d [CuuTruyen] Fix 2024-09-26 13:31:37 +03:00
955c75a99f [CuuTruyen] Fix attempt 1 2024-09-25 18:47:20 +03:00
devi
d32d1f5044 [ImHentai] add ContentTypes
add new ContentTypes
[iken] add SearchWithFilters, ContentTypes
[keyoapp] add SearchWithFilters
[likemanga] add POPULARITY_TODAY, POPULARITY_WEEK, POPULARITY_MONTH, SearchWithFilters
2024-09-25 16:10:29 +02:00
091c5247d5 Merge pull request #1093 from dragonx943/het-cuu-truyen
Fix decryptor + Add image reconstructor ?
2024-09-25 16:33:09 +03:00
Draken
c593798d83 Update NetTruyenSSR.kt 2024-09-25 19:32:55 +07:00
Draken
edac81f51a Update NetTruyenLL.kt 2024-09-25 19:32:26 +07:00
Draken
8da3b4b7ac Update NetTruyen.kt 2024-09-25 19:31:49 +07:00
Draken
844e5fdb50 Update Truyenqq.kt 2024-09-25 19:30:34 +07:00
Draken
3f502de1f7 Update Truyenqq.kt 2024-09-25 19:30:05 +07:00
Draken
6532f392b5 Update Truyenqq.kt 2024-09-25 19:29:39 +07:00
Draken
9f340b2dfb Update LxManga.kt 2024-09-25 19:28:50 +07:00
Draken
30d34de653 Update CuuTruyenParser.kt 2024-09-25 19:21:10 +07:00
devi
52329e658f [SoManga] close #1091
[AsuraComic] fix close #1088
[HeanCms] add SearchWithFilters
[fuzzydoodle] add ContentTypes, SearchWithFilters
[DoujinDesu.tv] add SearchWithFilters, ContentTypes
2024-09-25 14:18:43 +02:00
devi
1c0df02c56 [HiveToon] fix close #1086 2024-09-24 19:39:58 +02:00
devi
2061a971b8 [fmreader] add SearchWithFilters
[LegacyScans] add ContentTypes, SortOrder.UPDATED
[MangaMana] add SortOrder.RATING_ASC , SortOrder.NEWEST_ASC
[Manhwa18.com] move to /en
Add new option on .toAbsoluteUrl()
2024-09-24 19:35:22 +02:00
cc62981f12 [ComicK] Fix chapter numbers in names 2024-09-24 16:32:55 +03:00
613623fa53 Update information pages 2024-09-24 08:45:21 +03:00
devi
fffdbfdbee [MangaDex] fix close #1084 2024-09-23 19:05:31 +02:00
devi
a55a4720fe [TuMangaOnline] add SearchWithFilters, TagsExclusion, Demographics, ContentTypes
[TempleScanEsp] fix
Add KODOMO on Demographic
Add ONE_SHOT on ContentType
2024-09-23 18:57:08 +02:00
42f2813e44 Update readme 2024-09-23 16:01:26 +03:00
c58d35288b Added some codegen features 2024-09-23 15:45:02 +03:00
f410df40f1 Improve utilities 2024-09-22 10:52:45 +03:00
7e95949ab7 [Grouple] Improve filter 2024-09-22 08:37:55 +03:00
7c4c3a3c97 [ExHentai] Improve filtering and tags 2024-09-22 08:37:55 +03:00
devi
7f25c5f82d [Manga-Starz] Fix close #1073 2024-09-21 18:43:17 +02:00
devi
f1939391e8 [AsuraComic] Fix close #1077
add ContentTypes , SearchWithFilters
[CloneManga] isSearchSupported false
[MangaGeko] fix tags,  add MultipleTags, TagsExclusion
[Manhwa18.net] add isSearchSupported
[ManhwasMen] add Exception
[Pururin] add SearchWithFilters, MultipleTags , TagsExclusion
[VyManga] add SearchWithFilters, MultipleTags, TagsExclusion
2024-09-21 18:33:45 +02:00
devi
d1f9b0d829 [mangaDex] Fix SortOrder and remove YearRange
[Bato] add OriginalLocale
[MangaPark] add OriginalLocale, SearchWithFilters
[Comick] add SearchWithFilters
[NineMangaParser] add SearchWithFilters
[AnimeBootstrap] add SearchWithFilters , ContentTypes
[madara] add Year, SearchWithFilters, SortOrder.RELEVANCE
[TeamXNovel] add ContentTypes , SearchWithFilters
2024-09-21 11:27:04 +02:00
f2354957e6 [MangaDex] Fix filter capabilities 2024-09-21 11:29:32 +03:00
devi
600eab20a1 remove unessery code
Remove old chapter manga and change last source for new manga chapter
Simplify parseChapterDate
Fix fetchAvailableTags on some source need to override
2024-09-20 20:37:36 +02:00
336c4a4d49 Add year constants 2024-09-20 12:15:49 +03:00
a1e47edfb2 Fix getPageUrl visibility 2024-09-20 08:55:51 +03:00
5269659c73 Merge pull request #1063 from KotatsuApp/feature/advanced_filter
Advanced filter
2024-09-19 20:14:23 +03:00
ab1b549a64 Merge branch 'master' of github.com:KotatsuApp/kotatsu-parsers into feature/advanced_filter 2024-09-19 19:37:52 +03:00
devi
2867dffc5a Capabilities simplified on madara to avoid redundant code 2024-09-19 18:00:26 +02:00
6d972a13fe Fix filter capabilities/options default values 2024-09-19 11:12:55 +03:00
3918bfafdf Specify visibility modifiers explicitly 2024-09-19 10:45:47 +03:00
1c15e569bf Migration to MangaListFilterCapabilities 2024-09-19 10:20:53 +03:00
5030548500 Global refactoring: partial migrate to ListFilterOptions/ListFilterCapabilities 2024-09-18 16:52:35 +03:00
c5d3a7b0c1 Global refactoring: migrate getList to new filter 2024-09-18 15:22:19 +03:00
Draken
75c46130ed Update NetTruyenSSR.kt 2024-09-18 10:05:48 +03:00
Draken
62c17155ca Update NetTruyen.kt 2024-09-18 10:05:48 +03:00
Draken
3a52062ce4 Update NetTruyenLL.kt 2024-09-18 10:05:48 +03:00
Draken
74ccbdcaee Update HentaiCube.kt 2024-09-18 10:05:48 +03:00
Draken
fbd610349a Update PinkTeaComic.kt 2024-09-18 10:05:48 +03:00
Draken
8af75d8c6a Update TruyenVn.kt 2024-09-18 10:05:48 +03:00
Draken
0bafd117ff Update and rename HentaiVnCafe.kt to HentaiVnFit.kt 2024-09-18 10:05:48 +03:00
ae9a7c6090 Introduce MangaListFilterCapabilities class 2024-09-17 10:16:18 +03:00
481ad02e01 Partial migration to MangaListFilterV2 2024-09-17 10:10:22 +03:00
9042074c50 Merge remote-tracking branch 'origin/SortOrderPopularitytime' into feature/advanced_filter 2024-09-17 09:46:03 +03:00
821e51ff7d Merge branch 'filter-type-and-demographic' into feature/advanced_filter 2024-09-17 09:40:51 +03:00
0f4808f5b5 Use ContentType instead of Type 2024-09-17 09:35:54 +03:00
aba8a80d8f [MangaDex] Fix settings 2024-09-15 15:48:35 +03:00
2b0edfde60 [MangaDex] Data-saver server support (close #1062) 2024-09-14 14:27:31 +03:00
01a496768a MangaListFilter v2 2024-09-14 12:41:24 +03:00
Draken
82a57cb3c8 Update HentaiVN Domain 2024-09-14 12:30:37 +03:00
Naga
27c07d86bc fix tags 2024-09-14 12:29:28 +03:00
Naga
e916f2a66e Closes #1057 2024-09-14 12:29:28 +03:00
devi
494ecdfec8 Add year range. 2024-09-14 12:28:38 +03:00
devi
8a3c0e02f6 Add relevance sort order for good filters with text searches
Adding the added filter
2024-09-14 12:28:38 +03:00
devi
37428bf9c0 Add year, localeMangas filter on MangaListFilter.Advanced
Add year, localeMangas and localeMangas on MangaDexParser
2024-09-14 12:28:38 +03:00
devi
62b4a21cef Add query filter on MangaListFilter.Advanced
Add query.filer Advanced on MangaDexParser
2024-09-14 12:28:38 +03:00
davvarrr
b566e4e7e4 Update src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt
Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-09-14 11:25:23 +02:00
davvarrr
4ff2061bb7 Update src/main/kotlin/org/koitharu/kotatsu/parsers/MangaParser.kt
Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-09-14 11:25:16 +02:00
devi
03d7e138e9 Add support type and Demographic on MangaListFilter 2024-09-14 10:55:56 +02:00
devi
0540da57e6 Update Sources
Close #1060
2024-09-13 20:15:46 +02:00
devi
e0e9d25a4f Improve madara 2024-09-13 18:43:35 +02:00
devi
fd7684866e Refonte madara :
Add tagsExclude
Add support multiple tags
Add RATING_ASC ( and prepare RELEVANCE )
simplify postrequest
Preparation for query, year, author and artist filters
Change withoutAjax for some sources
2024-09-12 18:49:38 +02:00
devi
7c8b427d2a Miss add SortOrder in availableSortOrders for NHentaiParser 2024-09-10 14:31:19 +02:00
devi
4fcc68d149 add POPULARITY time sortOrder 2024-09-10 14:25:24 +02:00
ad726a3fd7 [CuuTruyen] Fixes 2024-09-05 12:30:27 +03:00
f167a8c8f6 [CuuTruyen] Fixes 2024-09-05 12:29:33 +03:00
Draken
b404b44008 insert something :p 2024-09-02 12:37:46 +03:00
Draken
c6dc51bb5d Some suggestions 2024-09-02 12:37:46 +03:00
Draken
69ee6be246 Create NetTruyenHE.kt 2024-09-02 12:37:46 +03:00
Draken
e08fab2938 Final Source commit:(
i cried:(
2024-09-02 12:37:46 +03:00
5db275bce2 [Grouple] Handle Usagi redirects 2024-08-31 16:03:32 +03:00
9c05e3f8e8 Usagi parser 2024-08-31 15:58:37 +03:00
devi
ac2c9b8621 Add InfamousScans 2024-08-31 11:28:59 +02:00
devi
02852ac4e7 Add Exclude Genres and new sort order on ComickFunParser
close #1032
2024-08-31 11:12:55 +02:00
devi
1163541ac5 Update sources and add sources
Fix MagusManga close #1021
2024-08-30 18:32:19 +02:00
devi
2da0c17fe2 Merge remote-tracking branch 'origin/master' 2024-08-29 17:46:36 +02:00
devi
7528480f54 add asc 2024-08-28 09:20:16 +03:00
devi
a45a270479 Add Asc on some SortOrder 2024-08-28 09:20:16 +03:00
devi
380804361b Add Asc on some SortOrder 2024-08-28 09:20:16 +03:00
devi
e072ee8b44 add asc 2024-08-23 18:41:56 +02:00
devi
d812644e61 Add Asc on some SortOrder 2024-08-23 18:41:38 +02:00
devi
71deb11e1f Add Asc on some SortOrder 2024-08-23 17:54:12 +02:00
devi
2e138da3d5 add source and fix 2024-08-23 17:32:53 +02:00
939b6b1e46 Improve TooManyRequestExceptions 2024-08-22 09:12:34 +03:00
d937c7e6ab [ExHentai] Fix IP ban detection 2024-08-20 17:17:46 +03:00
f91ff0b9d0 [ExHentai] Detect IP ban 2024-08-20 14:28:07 +03:00
98cbee11b9 Unify user agent header processing 2024-08-17 11:05:13 +03:00
Draken
7f0431d493 Update NetTruyenLL.kt 2024-08-17 09:46:39 +03:00
Draken
3227c53699 Create NetTruyenLL.kt 2024-08-17 09:46:39 +03:00
Draken
2ee8784d7d Update NetTruyen.kt 2024-08-17 09:46:39 +03:00
Draken
0fac0f450c Create BlogTruyenVNParser.kt 2024-08-17 09:46:39 +03:00
Draken
b4fb202db4 Update BlogTruyenParser.kt 2024-08-17 09:46:39 +03:00
Draken
93ba163ea2 Update NetTruyen.kt 2024-08-17 09:46:39 +03:00
Draken
bd3ff20146 Hotfix Source 2024-08-17 09:46:39 +03:00
devi
b3a0b97f0e add HastaTeamReader close #1004
Update Sources ( fix , url and broken )
Add sources
2024-08-16 12:58:51 +02:00
devi
ca212ca692 add auth on some sources
add Config Header on many sources
close #974
2024-08-11 18:06:07 +02:00
devi
3b809202b3 Merge pull request #994 from dragonx943/patch-1
Update HentaiVN Domain
2024-08-11 18:05:25 +02:00
Draken
9858419586 Update HentaiVN Domain 2024-08-11 21:57:15 +07:00
devi
1f7fe2aed3 Fix Sources
Add sources
Correct detect login on MadaraParser
close #988
fix oocini.biz close #987
Changing the latest DEPRECATION
( Technically we can raise the old support for getList() and MangaChapter() )
2024-08-11 16:15:16 +02:00
devi
71affd155c Some Fix 2024-08-06 18:20:35 +02:00
devi
a54f030c4e Fix, change url and add broken
Fix HeamCms Chapter close #970
add Template FuzzyDoodleParser
Fix LelScanVf
Add hentaislayer , ScyllaComics
Close #609
Close #901
Close #440
Add template IkenParser
Fix MangaGalaxyParser
Add VortexScans
Fix HniScantrad
Add HastaTeam close #939
Add HotComicsParser close #962
Add HentaiCrot close #913
2024-08-04 17:44:38 +02:00
devi
3b5a018f8c Fix, change url and add broken 2024-08-02 22:01:18 +02:00
devi
8d5fc945d4 Move ReaperComics to HeanCms
Rework heancms based on @NagaYZ's code
2024-08-02 19:27:23 +02:00
devi
50194df24d Rm duniakomik close #952
Fix DoujinKu close #951
Fix DoujinDesu.tv close
Add, fix some sources  #963
change url close #965 ( the copy was added anyway )
2024-08-02 17:38:04 +02:00
devi
5f771973a8 Merge pull request #967 from NagaYZ/fix-reaper
Fix reaperscan site structure changed
2024-08-02 17:31:06 +02:00
devi
4fcfbb374f Merge pull request #959 from dragonx943/patch-1
Broken source + Update domain
2024-08-02 17:30:51 +02:00
853c21e49f [ExHentai] Fix tags parsing 2024-08-01 21:18:55 +03:00
Naga
3efb5d8520 fixed page duplicated 2024-08-01 18:40:08 +02:00
Naga
e74985d870 Merge branch 'refs/heads/master' into fix-reaper
# Conflicts:
#	src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ReaperComics.kt
2024-08-01 18:14:02 +02:00
Naga
32eab42c26 Merge pull request #31 from KotatsuApp/master
[pull] master from KotatsuApp:master
2024-08-01 18:11:10 +02:00
Naga
0d3d8232f9 reaper changed site structure 2024-08-01 18:09:55 +02:00
Draken
fd3dc389df Update Saytruyenhay.kt 2024-08-01 00:48:54 +07:00
Draken
2d274f37ee Update TruyenTranhDamMyy.kt 2024-08-01 00:48:49 +07:00
Draken
d1a12df18f Create Saytruyenhay.kt 2024-08-01 00:42:27 +07:00
Draken
017a9a58a3 Update TruyenTranhDamMyy.kt 2024-08-01 00:36:19 +07:00
Draken
e33c57f51b Update Quaanhdaocuteo.kt 2024-07-31 02:06:55 +07:00
Draken
da041df054 Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/Saytruyenhay.kt 2024-07-31 02:05:13 +07:00
Draken
99b57e1297 Update TruyenTranhDamMyy.kt 2024-07-31 01:54:57 +07:00
Draken
6ce8a22514 Update BlogTruyenParser.kt 2024-07-31 01:48:51 +07:00
Draken
a9fc534ea7 Remove dead sources + Change domain (#955)
* Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/TruyentranhLHParser.kt

* Update NetTruyen.kt

* Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenAA.kt

* Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NetTruyenX.kt

* Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/Nettruyenmax.kt

* Update NetTruyen.kt

* Update NhatTruyenVN

* Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/NhatTruyenSS.kt
2024-07-30 09:27:41 +03:00
devi
6e04fa1251 Add CupFoxParser and sources
Add OneMangaParser and sources
2024-07-28 18:36:21 +02:00
devi
8f851282b4 Add XManhwa, MangaMana, MangaFr
Fix XoxoComics close #928
2024-07-28 01:08:01 +02:00
devi
d774935a6a Merge remote-tracking branch 'origin/master' 2024-07-27 11:28:31 +02:00
devi
712d42b328 Some Fix 2024-07-27 11:28:15 +02:00
SBS123
f66aab03f2 Updata MangaNoon (#924)
* Update Manjanoon.kt

* Update Manjanoon.kt

* Update Manjanoon.kt
2024-07-27 10:28:22 +03:00
devi
53ca9c9677 Rework WpComicsParser
Adds sources
2024-07-25 20:20:34 +02:00
devi
7bcc624a74 Add some sources
Fix some sources
close #929
2024-07-24 19:17:06 +02:00
devi
fa2d73348e rm @Broken 2024-07-23 18:38:12 +02:00
devi
26c3edbc17 Fix some sources
add @Broken on sources
Close #927
2024-07-23 18:36:21 +02:00
b9d89edfc2 [DynastyScans] Fixes 2024-07-23 10:04:46 +03:00
7ccc6438d5 [Grouple] Fix public url 2024-07-23 09:29:51 +03:00
devi
cd9f65596b Add some sources
Fix some sources
Add @Broken on some sources
close #909 #912 #920 #910 #914 #916
2024-07-20 19:57:45 +02:00
5404743f17 Update dependencies 2024-07-20 11:44:27 +03:00
c0296d0882 Fix build 2024-07-20 11:00:22 +03:00
Inkesk
dec07bd27d Update MangaGalaxy.kt
parser issue for mangagalaxy not loading chapter solved
2024-07-20 10:36:42 +03:00
Inkesk
1be1e9843d Update MangaGalaxy.kt
Updated the domain
2024-07-20 10:36:42 +03:00
Inkesk
5b5afcfe4c Update LuaScans.kt
Luascans.com changed domain to Luacomic.com , committing the change so future bugs and issues don't arise.
2024-07-20 10:36:42 +03:00
Inkesk
4f5dc9ffad Update ManhuaScan.kt
manhuascan.io was changed to kaliscan.io hence the change committed in here .
2024-07-20 10:36:42 +03:00
Inkesk
9c5b00ecf0 Update DarkScans.kt
Updated the domain name from darkscans.com to darkscans.net
2024-07-20 10:36:42 +03:00
Inkesk
048ba99323 Update ResetScans.kt
Changed the domain from reset-scans.xyz to reset-scans.co.
As previous domain for source was closed due to DMCA. Figure out the rest of details yourself , but resetscans is still on mates. Enjoy
2024-07-20 10:36:42 +03:00
devi
35bdc15aa9 Fix url grimelek 2024-07-15 18:49:19 +02:00
b06288e7eb Merge branch 'master' into experimental/abstract_sources 2024-07-15 14:58:19 +03:00
SBS123
426c7ad708 Update Normoyun.kt 2024-07-15 13:31:40 +03:00
devi
fadf8861e7 Rm BrMangas close #454
rm PowerManga close #462
Add MangaNinja close #897
Add Grimelek close #375
Add OpiaToon close#374
Add login on madara
Fix ManhwaFreak
2024-07-14 14:17:17 +02:00
devi
dceedf019a Fix HentaiVN
Remove some warning
2024-07-13 21:49:31 +02:00
devi
f49ddf7437 Fix HentaiVN
Remove some warning
2024-07-13 21:49:05 +02:00
devi
33a3c8350d Merge pull request #885 from dragonx943/patch-1
domain change hentaivn
2024-07-13 21:47:20 +02:00
devi
052fa146d2 Merge pull request #889 from NagaYZ/fix-manhuaplus
domain change manhuaplus
2024-07-13 21:33:34 +02:00
devi
1c85782690 Fix CafecomYaoi close #715
Fix Topmanhua close #710
Add Some sources
Add new Parser
fix EpsilonScan close #602
Add MangaHub.link close #892
Fix SussyScan close #893
Fix MaidScan close #585
fix HuntersScan close #584
Fix TeamXNovel close #575
close #574
Fix MangaTown close #569
Add LeitorDeManga close #864
add Search on ScanParser
Add mangabr close #862
Add Manga Italia close #841
Add Mangá Terra close #856
Fix MilaSub close #559
Fix KomikTapParser close #554
Fix YugenApp close #586
Fix TuMangaOnline close #566
Fix bato close #516
Fix Mangakakalot close #490
2024-07-13 21:30:59 +02:00
Draken
cf5abdf7d3 Update HentaiVNParser.kt 2024-07-12 16:22:44 +07:00
Naga
df562bedbb Closes #886 2024-07-12 00:38:19 +02:00
devi
11c1eafa0d Fix ReaperComics close #816
close #699
Fix GuildaTierDraw close #786
Fix Truyenqq close #793
Fix JiangzaiToon close #783
Fix MangaPark close #734
Fix ScanIta close #670
Add MangaPeak close #888
Fix ArvenComics close #660
2024-07-11 21:30:04 +02:00
Draken
cc8369e02a Update HentaiVNParser.kt 2024-07-11 23:49:45 +07:00
devi
cd468df9ad Add : MangaTx.to - AlterkaiScans - StoneScape
Kalango - SolooScan - ErosScans - SolooScan
RaysScan - TemakiMangas - ZinChanManga
Urls Changes
Add volume on some parser
Fix ThunderScans close #818
NinjaScan close #753
Add Ngomik close #761
Remove dulicate source paragonscans
Fix LuminousScans close #578
Fix YugenApp close #854
Fix KaiScans close #614
Add PeachBl Close #884
Fix GalinhaSamurai close #835
Fix RocksManga close #827
Fix CrystalComics close #792
2024-07-10 20:17:06 +02:00
Draken
64666e42e8 Update HentaiVNParser.kt
not sure, i hope it will works...
2024-07-09 01:31:14 +07:00
ad7c953d29 Merge pull request #883 from NagaYZ/fix-mangaworld 2024-07-08 09:03:07 +03:00
Naga
b345efe2d1 fixed source mangaworld pages 2024-07-07 20:53:16 +02:00
Naga
74b8aaa94e fixed source mangaworldadult 2024-07-07 12:06:48 +03:00
Naga
1ace1ba3ec added source mangaworldadult, refactor mangaworld 2024-07-07 12:06:48 +03:00
Naga
b822574b70 moved mangaworld in package 2024-07-07 12:06:48 +03:00
devi
466ca0f0e9 Merge remote-tracking branch 'origin/master' 2024-07-06 17:43:09 +02:00
devi
0bcac0c639 Add @Broken on some Dead Sources
Urls Changes
repair sources
Remove Duplicate Source
Add volume on some parser
2024-07-06 17:37:32 +02:00
7433fb8fa0 [LibSoc] Fix image server option 2024-07-06 12:23:37 +03:00
b1ac1cf238 [LibSoc] Fix image server option 2024-07-06 11:53:40 +03:00
Naga
a88a861d82 added main page sortby updated 2024-07-05 13:51:59 +03:00
Naga
41ced8edee added states , fixed title, added types 2024-07-05 13:51:59 +03:00
0f73f74539 [LibSoc] Default image server config option 2024-07-05 13:24:44 +03:00
bb9902e3b2 [LibSoc] Add split by translations config option 2024-07-05 13:24:41 +03:00
devi
c03b0fc981 Fix search on MangaDexParser
Fix State on ScansMangasMe
2024-07-03 21:50:18 +02:00
e8733f15e4 Merge pull request #853 from NagaYZ/fix-mangaworld 2024-06-30 15:52:27 +03:00
Naga
26519c71a6 fix #852 2024-06-25 21:44:40 +02:00
f923acc5a7 Extract MangaSource interface 2024-06-24 15:35:41 +03:00
Naga
55b9b6aac4 Merge pull request #26 from KotatsuApp/master
[pull] master from KotatsuApp:master
2024-06-23 17:33:07 +02:00
devi
52db07a33b MiHentai Closes #829
MrBenne Closes #627
Closes #555
Norterose Closes #625
Closes #564
Closes #483
MaidSecret Closes #623
Closes #550
HentaiOrigines
Fix SortOrder On MmrcmsParser
2024-06-22 18:18:19 +02:00
7ed8c9f787 [AllHen] Update domain 2024-06-22 12:08:05 +03:00
cc12b193d3 [MangaWorld] Refactor 2024-06-22 12:04:14 +03:00
4d9c51bcd9 [MangaWtf] Parser 2024-06-22 11:55:10 +03:00
Naga
7de48393b6 added source MangaWorld 2024-06-22 11:55:02 +03:00
Naga
283aa92fd5 fixed url and chapters 2024-06-22 11:52:08 +03:00
Naga
9d0393ca72 update domain 2024-06-22 11:30:21 +03:00
c5fc7f76d3 Merge pull request #845 from NagaYZ/fix-aquamanga 2024-06-19 11:14:50 +03:00
Naga
6b6325895b update aquamanga domain 2024-06-19 01:39:12 +02:00
39e9f5a2ff Added UNKNOWN manga source constant 2024-06-17 21:24:34 +03:00
204223e2a8 Merge pull request #837 from NagaYZ/fix-baozimh 2024-06-17 08:48:18 +03:00
Naga
b39abcc4a0 fix select chapter 2024-06-15 14:17:19 +02:00
SBS123
f49e9fa66b Update MangaPro.kt 2024-06-15 10:43:00 +03:00
scaledzdn
56fd22b43f src: id: shinigami: Change url to shinigamitoon.id
* Previously not working.
2024-06-07 08:27:51 +03:00
0b2bf607f7 Make locale non-nullable 2024-06-04 10:02:38 +03:00
3ff9e69585 [MangaOVH] Removal (closes #824) 2024-06-04 00:14:39 +03:00
9ceb90204c Merge pull request #801 from OtakuArab/patch-3 2024-06-03 17:28:17 +03:00
SBS123
ab3c4b2b2d Update and rename PewPiece.kt to Gmanga.kt 2024-06-01 12:49:11 +03:00
SBS123
060ca4b7ef Update MangaLinkNet.kt 2024-06-01 12:36:48 +03:00
SBS123
a7a29ef029 Rename LinkManga.kt to MangaLinkNet.kt 2024-06-01 12:34:08 +03:00
SBS123
e7826c2570 Update src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/LinkManga.kt
Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-06-01 12:26:43 +03:00
SBS123
77a733a062 Update and rename EnAresManga.kt to FlAres.kt 2024-06-01 11:06:07 +03:00
SBS123
96bd99650f Update and rename RocksManga.kt to YuriMoonSub.kt 2024-06-01 11:06:07 +03:00
SBS123
2d1899f1f2 Update TeamXNovel.kt 2024-06-01 11:05:45 +03:00
SBS123
42b1740b0f Update FlixScans.kt 2024-06-01 11:05:45 +03:00
SBS123
fbf613c4e6 Update and rename MangalinkParser.kt to LekMangaOrg.kt 2024-06-01 11:05:45 +03:00
SBS123
bf18070bc1 Update and rename MangaLekNet.kt to MangaLeko.kt 2024-06-01 11:05:45 +03:00
SBS123
c9c41fdd33 Create StellarSaber.kt 2024-06-01 11:05:45 +03:00
SBS123
949d822385 Delete src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/ar/StellarSaber.kt 2024-06-01 11:05:45 +03:00
SBS123
83b8f28055 Create MangaPro.kt 2024-06-01 11:05:45 +03:00
SBS123
c602d817d9 Create CrowScans.kt 2024-06-01 11:05:45 +03:00
SBS123
a50b217b6f Update and rename MangaLekCom.kt to LekMangaCom.kt 2024-06-01 11:05:45 +03:00
SBS123
23c031780f Update and rename Mangaatrend.kt to ManhaTok.kt 2024-06-01 11:05:45 +03:00
SBS123
e3b50d5dfa Update and rename KingOfShojo.kt to Hijalacom.kt 2024-06-01 11:05:45 +03:00
SBS123
9b1bee567f Update and rename KingOfManga.kt to MangaAtrend.kt 2024-06-01 11:05:45 +03:00
SBS123
3534973357 Create RocksManga.kt 2024-06-01 11:05:45 +03:00
SBS123
c4c4bdd547 Update Source 2024-06-01 11:04:43 +03:00
SBS123
528fadceb8 Update UmiManga.kt 2024-06-01 11:04:18 +03:00
SBS123
e15aecaa52 Update and rename BeastScans.kt to UmiManga.kt 2024-06-01 11:04:18 +03:00
SBS123
62e635ebe5 Update MangaTime.kt 2024-06-01 11:03:58 +03:00
SBS123
be846b43ae Update Mangaspark.kt 2024-06-01 11:03:58 +03:00
SBS123
7bfe168eee Update MangaLionz.kt 2024-06-01 11:03:58 +03:00
SBS123
e8d4a91ad7 Update MangaStarz.kt 2024-06-01 11:03:58 +03:00
SBS123
bc1804c030 Update and rename LikeManga.kt to LikeMangaNet.kt 2024-06-01 11:03:20 +03:00
SBS123
d18c9fe0da Update LikeManga.kt 2024-06-01 11:03:20 +03:00
SBS123
a7ea87dd8c Update AzoraMoon.kt 2024-06-01 11:03:20 +03:00
SBS123
e3a0ab8513 Update LikeManga.kt 2024-06-01 11:03:20 +03:00
SBS123
3a77144a5e Update LikeManga.kt 2024-06-01 11:03:20 +03:00
SBS123
69e311829b Update and rename MangaLike.kt to LikeManga.kt 2024-06-01 11:03:20 +03:00
SBS123
b1effdb7c7 Update and rename Azoranov.kt to AzoraMoon.kt 2024-06-01 11:03:20 +03:00
SBS123
124537eb18 Create ArAreaScans.kt 2024-06-01 11:02:06 +03:00
SBS123
b81924b144 Update Manjanoon.kt 2024-06-01 10:59:27 +03:00
Naga
dd18605678 cleanup 2024-06-01 10:59:06 +03:00
Naga
6bce31d43f fix title selector 2024-06-01 10:59:06 +03:00
Naga
0da5b47ce5 fetching code using suspendlazy 2024-06-01 10:55:15 +03:00
Naga
d0d891b112 Apply suggestions from code review
Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-06-01 10:55:15 +03:00
Naga
2292c392e9 fix filters, added search support (#775) 2024-06-01 10:55:15 +03:00
cc35aa6bc3 Broken parser annotation 2024-06-01 10:53:59 +03:00
SBS123
9e4a124a58 Update ScarManga.kt 2024-05-28 21:35:30 +03:00
SBS123
d63afb0917 Rename LikManga.kt to LinkManga.kt 2024-05-28 19:15:03 +03:00
SBS123
80a3f61a1f Create ArAreaScans.kt 2024-05-28 15:05:02 +03:00
SBS123
303c817d2d Update and rename MangalinkNet.kt to LikManga.kt 2024-05-28 14:24:14 +03:00
SBS123
d9cf94c6b5 Update LekManga.kt 2024-05-27 20:18:46 +03:00
SBS123
d72414d531 Update and rename AresManga.kt to ScarManga.kt 2024-05-27 20:13:56 +03:00
26be293f24 [ComicK] Fix volume & chapter numbers 2024-05-27 13:18:15 +03:00
SBS123
36b8633c1e Update and rename MangaLek.kt to LekManga.kt 2024-05-27 13:05:55 +03:00
Naga
6bdbf2c65e closes #795 2024-05-27 13:05:34 +03:00
80ec615548 [Comick] Fix "No value for hentai" 2024-05-26 14:19:32 +03:00
ffc45ff79b Merge pull request #777 from AwkwardPeak7/patch-1 2024-05-25 18:48:16 +03:00
AwkwardPeak7
48cb28d9f8 Update MangaFireParser.kt 2024-05-25 18:33:38 +05:00
51da0b62c1 Apply suggestions from code review 2024-05-25 16:23:31 +03:00
AwkwardPeak7
e391ed52f3 MangaFire: related manga author language filter 2024-05-25 16:23:31 +03:00
AwkwardPeak7
a5e6e4255d MangaFire: tag exclusion 2024-05-25 16:23:31 +03:00
AwkwardPeak7
590e7e3ba3 add MangaFire 2024-05-25 16:23:31 +03:00
d218ad5a67 [MangaReaderTo] Syncronize descrambling 2024-05-23 16:58:10 +03:00
350bc0ad58 Small refactor 2024-05-23 16:26:49 +03:00
AwkwardPeak7
a0f9bc032d cleanup and test impl 2024-05-23 16:05:14 +03:00
AwkwardPeak7
4edcd70871 simplify api 2024-05-23 16:05:14 +03:00
AwkwardPeak7
90c0bf46f4 image redrawing api 2024-05-23 16:05:14 +03:00
AwkwardPeak7
9d4fc1980f MangaReader.To 2024-05-23 16:05:14 +03:00
Naga
332524f1f3 new source added modescanlator, closes #759 2024-05-23 16:03:43 +03:00
078b59b1e2 [Madara] Migrate to ScatterSet and add UserAgent config option 2024-05-15 19:46:48 +03:00
0551ed5f0b [MangaDex] Chapters fixes 2024-05-15 17:25:09 +03:00
915d4093b9 Fixes 2024-05-14 12:51:48 +03:00
952e9c39ac [LibSocial] Rewrite parsers 2024-05-14 12:33:24 +03:00
3e32a6280a [LuratoonScan] Rewrite parser 2024-05-12 13:12:51 +03:00
288b67e250 Close #741 2024-05-09 11:43:32 +03:00
7d2f5696f5 Close #719 2024-05-09 01:16:18 +03:00
8c3fec0933 Close #677 2024-05-09 01:15:48 +03:00
33b00fe65f Fixes #728 #725 2024-05-08 12:35:38 +03:00
f0f50a37b5 Fixes batch #733 #730 #731 #727 2024-05-08 10:26:29 +03:00
c2b2148190 Upgrade gradle 2024-05-06 18:03:20 +03:00
f22362dc53 [Grouple] Fix alt titles parsing 2024-05-06 17:55:21 +03:00
68cc1d4c4f [DesuMe] Improvements 2024-05-06 17:46:54 +03:00
0f84ef1e58 [Desu] Fix list loading 2024-05-04 21:26:59 +03:00
Naga
a245574dee fix #702 2024-04-21 16:32:51 +03:00
fd90970173 [MadTheme] Improve pages parsing 2024-04-21 16:31:22 +03:00
75a55e4748 [SinensisScans] Update domain #674 2024-04-20 17:57:38 +03:00
bbd4867830 [FbSquads] Update domain #681 2024-04-20 17:55:15 +03:00
3463c8a49e [HeanCms] Fixes 2024-04-20 17:52:17 +03:00
7829a2ad3b [MadTheme] Fix pages parsing #701 2024-04-20 16:53:23 +03:00
AwkwardPeak7
fb387dbcd9 FreakComic: update selectors 2024-04-14 08:47:06 +03:00
cd0b3cd1dc Update gitignore 2024-04-14 08:46:46 +03:00
fe500a27c0 Update dependencies 2024-04-14 08:43:24 +03:00
da860bc250 Fix imports 2024-04-14 08:32:42 +03:00
e3a8eeb647 Update src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/en/OmegaScans.kt
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-04-13 15:15:03 +03:00
Naga
6dbc709f98 fix #687 2024-04-13 15:15:03 +03:00
AwkwardPeak7
49ca54d68a MangaGalaxy: move to MangaReader 2024-04-13 15:13:31 +03:00
AwkwardPeak7
e81b8127af DrakeScans: move to MangaReader 2024-04-13 15:13:31 +03:00
AwkwardPeak7
d1b490ed98 add german to MangaPlus 2024-04-13 15:13:31 +03:00
AwkwardPeak7
893ed6afd2 999Hentai: fix api url 2024-04-13 15:13:31 +03:00
9df2cf5bd7 [AstrumScans] Source removal on request (closes #682) 2024-04-07 12:38:21 +03:00
b5ceaf4e65 Add OtakuWorld to README 2024-04-04 21:34:21 +03:00
Naga
44ea9fe709 fix #667 2024-03-30 09:06:15 +02:00
8174229d28 Merge pull request #658 from NagaYZ/fix-xoxocomics
fix chapter order
2024-03-30 09:05:54 +02:00
f628ed2e1b Add kotatsu-dl to README 2024-03-26 14:26:51 +03:00
c90a0a77bc Update README 2024-03-26 14:24:07 +03:00
Naga
10b3f2e65b fix chapter order 2024-03-23 16:11:08 +01:00
9821e93d25 Merge pull request #655 from NagaYZ/fix-xoxocomics
fix #652
2024-03-23 16:24:15 +02:00
Naga
6c16c9c55f fix #652 2024-03-22 18:57:49 +01:00
ab679bccc6 Merge pull request #647 from NagaYZ/fix-issue
Fix Omegscans
2024-03-22 06:58:40 +02:00
Naga
6fcf3785ef fix #591 2024-03-20 10:10:54 +01:00
Naga
e605b325c7 fixrating 2024-03-19 21:27:28 +01:00
Naga
a714d0927e fix OmegaScans tags 2024-03-19 20:46:15 +01:00
Naga
fe1ef89d30 fix OmegaScans site structure changed 2024-03-19 19:56:25 +01:00
639895f511 MangaOVH: Parsing related manga 2024-03-19 12:30:35 +02:00
4fdc02de35 MangaOVH: Fix handling unexisting branches 2024-03-19 11:37:34 +02:00
103ef11f3d Dynamic UserAgent support 2024-03-18 12:23:09 +02:00
69e7efe6d1 NHentai: configurable User-Agent 2024-03-18 11:05:47 +02:00
83e971d85b Update UserAgents 2024-03-18 09:49:40 +02:00
fec60955ed Merge pull request #631 from NagaYZ/fix-issue
Fix source domain and issue
2024-03-15 10:44:45 +02:00
c5b980a406 Update issue templates 2024-03-15 10:40:15 +02:00
99483268d6 Remove Bakai parser #629 2024-03-15 10:29:23 +02:00
Naga
67fef2cc1c fix mangatown select and pagination 2024-03-14 19:08:47 +01:00
Naga
f450111c1c fix Baozimh webview and 50+ page chapter 2024-03-14 19:08:18 +01:00
Naga
718e7eab82 fix source domain 2024-03-14 19:07:50 +01:00
eb031b00ab ComicK: Update domain 2024-03-14 17:16:00 +02:00
a10c1101ab New source: MangaOVH 2024-03-14 17:12:18 +02:00
Naga
9c0c20f86b Merge remote-tracking branch 'upstream/master' 2024-03-14 09:38:56 +01:00
c6d1f1b525 [NepNep] Refactor #628 2024-03-13 12:56:14 +02:00
9a84791f5c Merge pull request #628 from NagaYZ/feature-manga4life-sort
Feature manga4life sort
2024-03-13 12:53:47 +02:00
14fc02cb23 [ReaperComics] Refactor #620 2024-03-13 12:53:34 +02:00
39ae6a406c [Grouple] Added Seimanga support 2024-03-13 12:45:54 +02:00
ba8682f79e Merge pull request #620 from NagaYZ/feature-search-reaper
Feature search reaper
2024-03-13 12:43:19 +02:00
Naga
986e4d8fcc Merge pull request #1 from KotatsuApp/master
[pull] master from KotatsuApp:master
2024-03-13 00:51:39 +01:00
Naga
016ced24e0 added pagination 2024-03-13 00:13:02 +01:00
Naga
b60b2d8355 added sort by popularity/updated 2024-03-12 23:11:18 +01:00
Naga
f733b85878 fix webtoon sort newest 2024-03-12 23:10:31 +01:00
Naga
1926a73dee without serialization, and added cache 2024-03-11 20:01:07 +01:00
071f4f0911 Trigger rebuild 2024-03-11 11:41:32 +02:00
813c5236b3 [Grouple] Back to the old chapters parsing behavior with option 2024-03-11 09:27:04 +02:00
Naga
f39a9f191a cleanup 2024-03-10 23:39:31 +01:00
Naga
6d8d757798 fix manga chapters list 2024-03-10 23:38:28 +01:00
Naga
81975977ad added search support for reaperscans 2024-03-09 23:44:01 +01:00
b7613606c0 [TuMangaOnline] Fix regexp 2024-03-09 14:03:09 +02:00
0aa4ea01f7 [Grouple] Multiple translations support 2024-03-09 12:10:04 +02:00
20685598e3 [TuMangaOnline] Fix pages parsing 2024-03-09 11:22:30 +02:00
b1fb1bdc6b Merge pull request #615 from NagaYZ/feature-search-by-number
Feature search by number
2024-03-09 11:04:13 +02:00
Naga
a80bdcc611 full title name 2024-03-08 21:18:52 +01:00
Naga
4571e3e001 gitignore 2024-03-08 02:31:59 +01:00
Naga
38d7d97167 added search by id 2024-03-08 02:29:59 +01:00
3913e95b53 Merge pull request #608 from nghduc97/fix-mangabox-parser-white-spaces
fix mangaboxparser white space coding in search query
2024-03-04 11:56:07 +02:00
954c926a9f Merge pull request #588 from NagaYZ/feature-webtoon-original
Feature webtoon original
2024-03-04 11:55:52 +02:00
nghduc97
0973444c47 fix mangaboxparser white space coding in search query 2024-03-03 11:45:10 +07:00
Naga
368b61c21a fixed suggestions 2024-02-28 18:13:51 +01:00
Naga
efc9b3502c fixed suggestions 2024-02-28 18:03:12 +01:00
Naga
2e86c480ec Update src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/WebtoonsParser.kt
Co-authored-by: Koitharu <nvasya95@gmail.com>
2024-02-28 16:01:36 +01:00
Naga
39eeaacdcb fixup! trying to improve speed 2024-02-25 16:32:31 +01:00
Naga
ea45e91aa7 trying to improve speed 2024-02-25 14:47:06 +01:00
Naga
a65e41e796 fix 2024-02-25 14:24:14 +01:00
Naga
91648756c9 added more language 2024-02-25 14:16:24 +01:00
Naga
a225234800 fixed chapter date 2024-02-25 14:11:30 +01:00
Naga
de58333dc4 fixup! fixup! fixed offset 2024-02-25 13:12:09 +01:00
Naga
24a25e3dbf fixup! fixed offset 2024-02-25 13:00:31 +01:00
Naga
8e5ae51067 fixed offset 2024-02-25 12:50:49 +01:00
Naga
86c981ae8e fixup! trying to speed up loading 2024-02-25 12:33:11 +01:00
Naga
b716c35438 trying to speed up loading 2024-02-25 12:30:11 +01:00
Naga
9e984510d4 added caching for title list to speed up loading 2024-02-25 03:30:00 +01:00
Naga
3c08119ccc added caching for title list to speed up loading 2024-02-25 03:12:12 +01:00
Naga
ada1b7b54c fixed sort descending 2024-02-25 00:38:17 +01:00
Naga
a7ed4fbcc3 fix import 2024-02-24 23:33:10 +01:00
Naga
1330feaea5 added webtoon original english support 2024-02-24 23:32:37 +01:00
103f578c61 Merge pull request #579 from NagaYZ/master
Update domains name
2024-02-24 12:19:58 +02:00
deee80ae71 Merge pull request #581 from NagaYZ/fix-popular-page-url
Fix popular page url
2024-02-24 12:19:37 +02:00
Naga
54350040ad fixed popular page url 2024-02-22 20:32:21 +01:00
Naga
3472b8ba00 Update ZinManga.kt 2024-02-22 17:09:36 +01:00
Naga
f2dd9af66c Update MagusManga.kt
fixed domain name and listurl acces
2024-02-22 16:56:55 +01:00
Naga
da46f7967a fixed bunch of domain name 2024-02-22 16:56:25 +01:00
Naga
ed3d6c5f98 Replaced GloryScans/GloryScansX with LunarScans/LunarHentai
domain changed
2024-02-22 16:55:28 +01:00
Naga
c4ea825df9 Replaced LightScans with Luascans , domain name changed 2024-02-22 16:54:49 +01:00
Naga
4120e3946c Update RagnarokScanlation.kt
new domain: ragnarokscanlation.net
2024-02-22 16:54:09 +01:00
Naga
c84b6acff2 Update EmperorScan.kt
new domain : emperorscan.com
2024-02-22 16:53:58 +01:00
Naga
159110c032 Update ResetScans.kt
new domain: reset-scans.xyz
2024-02-22 16:53:46 +01:00
Naga
93e1b38da2 Update MangaEsp.kt
new domain : mangaesp.net
2024-02-22 16:53:28 +01:00
3ff028c4e9 Merge pull request #572 from geexuan/baozimh_details_fix
Baozimh: Fix for not getting all chapter details
2024-02-18 08:25:57 +02:00
geexuans
9b46963015 Baozimh: Fix for not getting all chapter details 2024-02-17 16:04:24 +08:00
09e1c14f37 Merge pull request #565 from sandeepjak2007/Issue##561
Issue #561 Leading to non existing page
2024-02-14 12:03:01 +02:00
Sandeep Karanam
58af01b90a Issue #561 Leading to non existing page
Issue #561 fixed
2024-02-14 09:23:38 +05:30
e338237e2a [NudeMoon] Fix pages loading 2024-02-12 16:02:27 +02:00
014ea5ef49 [Manganato] Update domain 2024-02-10 14:30:10 +02:00
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
1504 changed files with 59458 additions and 14631 deletions

View File

@@ -3,12 +3,18 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
indent_style = space
indent_size = 4
max_line_length = 120
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
# noinspection EditorConfigKeyCorrectness
disabled_rules = no-wildcard-imports, no-unused-imports
[{*.kt,*.kts}]
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
[*.md]
indent_size = 2
trim_trailing_whitespace = false

View File

@@ -24,6 +24,7 @@ body:
1. First step
2. Second step
3. Issue here
Please use English language
validations:
required: false

View File

@@ -1,30 +1,31 @@
name: ⭐ Feature request
description: Suggest a feature to improve a source
labels: [feature request]
labels: [ feature request ]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can an existing source be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can an existing source be improved?
placeholder: |
Example:
"It should work like this..."
Please use English language
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true

View File

@@ -1,33 +1,31 @@
name: 🗑 Source removal request
description: Scanlators can request their site to be removed
labels: [source removal]
labels: [ source removal ]
body:
- type: input
id: link
attributes:
label: Source link
placeholder: |
Example: "https://example.org"
validations:
required: true
- type: input
id: link
attributes:
label: Source link
placeholder: |
Example: "https://example.org"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: textarea
id: other-details
attributes:
label: Other details (reason for removal, etc)
placeholder: |
Additional details and attachments.
- type: checkboxes
id: requirements
attributes:
label: Requirements
description: Your request will be denied if you don't meet these requirements.
options:
- label: Proof of ownership/intent to remove sent to a Kotatsu Discord server mod via DM
required: true
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
required: true
- label: Site is not infested with user-hostile features (e.g., invasive or malicious ads)
required: true
- type: checkboxes
id: requirements
attributes:
label: Requirements
description: Your request will be denied if you don't meet these requirements.
options:
- label: Proof of ownership of the website is sent to a Kotatsu [Discord server](https://discord.gg/NNJ5RgVBC5) or [Telegram community](https://t.me/kotatsuapp)
required: true
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
required: true

1
.github/summary.yaml vendored Normal file
View File

@@ -0,0 +1 @@
total: 1251

27
.github/workflows/test-branch.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Check & Test latest parsers
on:
push:
branches:
- master
jobs:
check-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository 🌏
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up enviroment 🔧
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle 📦
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with:
cache-read-only: true
- name: Compile parsers 🚀
run: ./gradlew compileKotlin

View File

@@ -1,10 +1,10 @@
name: Parsers test
name: Parsers test for PRs
on:
workflow_dispatch:
pull_request:
paths:
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/site/**'
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
permissions:
contents: read
@@ -13,15 +13,19 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '11'
- name: Checkout repository 🌏
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up enviroment 🔧
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
java-version: '17'
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
- name: Set up Gradle 📦
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with:
cache-read-only: true
- name: Compile parsers 🚀
run: ./gradlew compileKotlin

17
.gitignore vendored
View File

@@ -4,6 +4,7 @@
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/copilot
# Generated files
.idea/**/contentModel.xml
@@ -16,6 +17,7 @@
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/Project_Default.xml
# Gradle
.idea/**/gradle.xml
@@ -25,10 +27,13 @@
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/deviceManager.xml
.idea/.name
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/ktlint-plugin.xml
.idea/*.iml
.idea/modules
*.iml
@@ -69,11 +74,21 @@ fabric.properties
.gradle/
build/
bin/
.idea/**/misc.xml
.idea/**/vcs.xml
.idea/**/ktlint.xml
.idea/codeStyles/
.idea/kotlinc.xml
src/test/resources/cookies.txt
local.properties
local.properties
.kotlin/
!/.idea/kotlin-statistics.xml
.idea/**/discord.xml
.idea/**/migrations.xml
.idea/**/runConfigurations.xml
.idea/**/AndroidProjectSystem.xml
.idea/caches/deviceStreaming.xml

5
.idea/.gitignore generated vendored
View File

@@ -1,3 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
.name
deviceManager.xml

6
.idea/kotlinc.xml generated
View File

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

View File

@@ -1,10 +1,10 @@
# Contributing
The following is guide for creating a Kotatsu parsers. Thanks for taking the time to contribute!
The following is a guide for creating Kotatsu parsers. Thanks for taking the time to contribute!
## Prerequisites
Before you start, please note that the ability to use following technologies is **required**.
Before you start, please note that the ability to use the following technologies is **required**.
- Basic [Android development](https://developer.android.com/)
- [Kotlin](https://kotlinlang.org/)
@@ -16,24 +16,24 @@ Before you start, please note that the ability to use following technologies is
- [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community edition is enough)
- Android device (or emulator)
Kotatsu parsers is not a part of Android application, but you can easily develop and test it directly inside an Android
application project and relocate it to the library project when done.
Kotatsu parsers are not a part of the Android application, but you can easily develop and test it directly inside an
Android application project and relocate it to the library project when done.
### Before you start
First, take a look at `kotatsu-parsers` project structure. Each parser is a single class that
extends `MangaParser` class and have a `MangaSourceParser` annotation.
Also pay attention on extensions in `util` package. For example, extensions from `Jsoup` file
First, take a look at the `kotatsu-parsers` project structure. Each parser is a single class that
extends the `MangaParser` class and has a `MangaSourceParser` annotation.
Also, pay attention to extensions in the `util` package. For example, extensions from the `Jsoup` file
should be used instead of existing JSoup functions because they have better nullability support
and improved error messages.
## Writing your parser
So, you want to create a parser, that will provide access to manga from a website.
First, you should explore a website for API availability.
First, you should explore a website to learn about API availability.
If it does not contain any documentation about
API, [explore network requests](https://firefox-source-docs.mozilla.org/devtools-user/):
some websites use ajax.
some websites use AJAX.
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/DesuMeParser.kt)
of Json API usage.
@@ -42,43 +42,48 @@ some websites use ajax.
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/MangaTownParser.kt)
of pure HTML parsing.
If website is based on some engine it is rationally to use common base class for this one (for example, Madara wordress
theme
and the `MadaraParser` class)
If the website is based on some engine it is rationally to use a common base class for this one (for example, Madara
Wordpress theme and the `MadaraParser` class)
### Parser class skeleton
Parser class must have exactly one primary constructor parameter of type `MangaLoaderContext` and have an
`MangaSourceParser` annotation that provides internal name, title and language of a manga source.
The parser class must have exactly one primary constructor parameter of type `MangaLoaderContext` and have an
`MangaSourceParser` annotation that provides the internal name, title, and language of a manga source.
All functions in `MangaParser` class are documented. Pay attention to some peculiarities:
All members of the `MangaParser` class are documented. Pay attention to some peculiarities:
- Never hardcode domain. Specify default domain in `configKeyDomain` field and obtain an actual one using `getDomain()`.
- All ids must be unique and domain-independent. Use `generateUid` functions with relative url or some internal id which
is unique across the manga source.
- `sortOrders` set should not be empty. If your source is not support sorting, specify one most relevance value.
- If you cannot obtain direct links to pages images inside `getPages` method, it is ok to use an intermediate url
as `Page.url` and fetch a direct link at `getPageUrl` function.
- You can use _asserts_ to check some optional fields. For example. `Manga.author` field is not required, but if your
source provide such information, add `assert(it != null)`. This will not have any effect on production but help to
find issues during unit testing.
- If your source website (or it's api) uses pages for pagination instead of offset you should extend `PagedMangaParser`
instead of `MangaParser`.
- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and/or
- Never hardcode domain. Specify the default domain in the `configKeyDomain` field and obtain an actual one using
`domain`.
- All IDs must be unique and domain-independent. Use `generateUid` functions with a relative URL or some internal id
that is unique across the manga source.
- The `availableSortOrders` set should not be empty. If your source does not support sorting, specify one most relevant
value.
- If you cannot obtain direct links to page images inside the `getPages` method, it is ok to use an intermediate URL
as `Page.url` and fetch a direct link in the `getPageUrl` function.
- You can use _asserts_ to check some optional fields. For example, the `Manga.author` field is not required, but if
your source provides this information, add `assert(it != null)`. This will not have any effect on production but help
to find issues during unit testing.
- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and
responses, including image loading.
- If your source website (or its API) uses pages for pagination instead of offset you should extend `PagedMangaParser`
instead of `MangaParser`.
- If your source website (or its API) does not provide pagination (has only one page of content) you should extend
`SinglePageMangaParser` instead of `MangaParser` or `PagedMangaParser`.
![parser_classes.png](docs/parser_classes.png)
## Development process
During the development it is recommended (but not necessary) to write it directly
in the Kotatsu android application project. You can use `core.parser.DummyParser` class as a sandbox. `Dummy` manga
source is available in debug Kotatsu build.
During the development, it is recommended (but not necessary) to write it directly
in the Kotatsu Android application project. You can use the `core.parser.DummyParser` class as a sandbox. The `Dummy`
manga source is available in the debug Kotatsu build.
Once parser is ready you can relocate your code into `kotatsu-parsers` library project in a `site` package and create a
Pull Request.
Once the parser is ready you can relocate your code into the `kotatsu-parsers` library project in a `site` package and
create a Pull Request.
### Testing
It is recommended to run unit tests before submitting a PR.
It is recommended that unit tests be run before submitting a PR.
- Temporary modify the `MangaSources` annotation class: specify your parser(s) name(s) and change mode
to `EnumSource.Mode.INCLUDE`
@@ -87,5 +92,5 @@ It is recommended to run unit tests before submitting a PR.
## Help
If you need a help or have some questions, ask a community in our [Telegram chat](https://t.me/kotatsuapp)
If you need help or have some questions, ask a community in our [Telegram chat](https://t.me/kotatsuapp)
or [Discord server](https://discord.gg/NNJ5RgVBC5).

View File

@@ -1,8 +1,9 @@
# Kotatsu parsers
This library provides manga sources.
This library provides a collection of manga parsers for convenient access manga available on the web. It can be used in
JVM and Android applications.
[![](https://jitpack.io/v/KotatsuApp/kotatsu-parsers.svg)](https://jitpack.io/#KotatsuApp/kotatsu-parsers) ![Kotlin](https://img.shields.io/github/languages/top/KotatsuApp/kotatsu-parsers) ![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF)](https://t.me/kotatsuapp) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C) [![](https://jitpack.io/v/KotatsuApp/kotatsu-parsers.svg)](https://jitpack.io/#KotatsuApp/kotatsu-parsers) ![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF)](https://t.me/kotatsuapp) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
## Usage
@@ -37,19 +38,31 @@ 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
val parser = mangaLoaderContext.newParserInstance(MangaSource.MANGADEX)
val parser = mangaLoaderContext.newParserInstance(MangaParserSource.MANGADEX)
```
`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/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt)
implementation.
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.
## Projects that use the library
- [Kotatsu](https://github.com/KotatsuApp/Kotatsu)
- [Doki](https://github.com/DokiTeam/Doki)
- [kotatsu-dl](https://github.com/KotatsuApp/kotatsu-dl)
- [Shirizu (WIP)](https://github.com/ztimms73/shirizu)
- [OtakuWorld](https://github.com/jakepurple13/OtakuWorld)
## Contribution

View File

@@ -1,72 +0,0 @@
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 'maven-publish'
}
group = 'org.koitharu'
version = '1.0'
test {
useJUnitPlatform()
}
compileKotlin {
kotlinOptions {
freeCompilerArgs += [
'-opt-in=kotlin.RequiresOptIn',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
]
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs += [
'-opt-in=kotlin.RequiresOptIn',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
]
}
}
kotlin {
jvmToolchain(11)
sourceSets {
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
}
}
afterEvaluate {
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
}
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 'org.json:json:20231013'
implementation 'androidx.collection:collection-ktx:1.3.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.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'io.webfolder:quickjs:1.1.0'
}
tasks.register('generateTestsReport', ReportGenerateTask)

63
build.gradle.kts Normal file
View File

@@ -0,0 +1,63 @@
import tasks.ReportGenerateTask
plugins {
`java-library`
`maven-publish`
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ksp)
}
group = "org.koitharu"
version = "1.0"
tasks.test {
useJUnitPlatform()
}
ksp {
arg("summaryOutputDir", "${projectDir}/.github")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi",
)
}
}
kotlin {
jvmToolchain(8)
explicitApiWarning()
sourceSets["main"].kotlin.srcDirs("build/generated/ksp/main/kotlin")
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
}
}
}
dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.okhttp)
implementation(libs.okio)
implementation(libs.json)
implementation(libs.androidx.collection)
api(libs.jsoup)
ksp(project(":kotatsu-parsers-ksp"))
testImplementation(libs.junit.api)
testImplementation(libs.junit.engine)
testImplementation(libs.junit.params)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.quickjs)
}
tasks.register<ReportGenerateTask>("generateTestsReport")

View File

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

18
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,18 @@
plugins {
kotlin("jvm") version "2.2.10"
}
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.10")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}

View File

@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

804
docs/parser_classes.gaphor Normal file
View File

@@ -0,0 +1,804 @@
<?xml version="1.0" encoding="utf-8"?>
<gaphor xmlns="https://gaphor.org/model" xmlns:Core="https://gaphor.org/modelinglanguage/Core" xmlns:UML="https://gaphor.org/modelinglanguage/UML" xmlns:general="https://gaphor.org/modelinglanguage/general" version="4" gaphor-version="3.1.0">
<model>
<Core:StyleSheet id="58d6989a-66f8-11ec-b4c8-0456e5e540ed"/>
<UML:Package id="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed">
<name>
<val>Новая модель</val>
</name>
<ownedDiagram>
<reflist>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</reflist>
</ownedDiagram>
<ownedType>
<reflist>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</ownedType>
<packagedElement>
<reflist>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</packagedElement>
</UML:Package>
<UML:Diagram id="58d6c536-66f8-11ec-b4c8-0456e5e540ed">
<element>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</element>
<name>
<val>Новая диаграмма</val>
</name>
<ownedPresentation>
<reflist>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</ownedPresentation>
</UML:Diagram>
<UML:Class id="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8">
<clientDependency>
<reflist>
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</clientDependency>
<comment>
<reflist>
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</comment>
<isAbstract>
<val>1</val>
</isAbstract>
<name>
<val>AbstractMangaParser</val>
</name>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<package>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</package>
<presentation>
<reflist>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
</reflist>
</presentation>
<specialization>
<reflist>
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</specialization>
</UML:Class>
<UML:ClassItem id="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 388.8671875)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>158.0</val>
</width>
<height>
<val>60.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<show_attributes>
<val>0</val>
</show_attributes>
<show_operations>
<val>0</val>
</show_operations>
<subject>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
</subject>
</UML:ClassItem>
<UML:Class id="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8">
<comment>
<reflist>
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</comment>
<generalization>
<reflist>
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</generalization>
<isAbstract>
<val>1</val>
</isAbstract>
<name>
<val>PagedMangaParser</val>
</name>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<package>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</package>
<presentation>
<reflist>
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Class>
<UML:ClassItem id="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 476.3368367667698, 525.76953125)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>142.0</val>
</width>
<height>
<val>60.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<show_attributes>
<val>0</val>
</show_attributes>
<show_operations>
<val>0</val>
</show_operations>
<subject>
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:ClassItem>
<UML:Class id="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8">
<comment>
<reflist>
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</comment>
<generalization>
<reflist>
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</generalization>
<isAbstract>
<val>1</val>
</isAbstract>
<name>
<val>SinglePageMangaParser</val>
</name>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<package>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</package>
<presentation>
<reflist>
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Class>
<UML:ClassItem id="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 627.46875)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>175.0</val>
</width>
<height>
<val>60.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<show_attributes>
<val>0</val>
</show_attributes>
<show_operations>
<val>0</val>
</show_operations>
<subject>
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:ClassItem>
<UML:GeneralizationItem id="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<subject>
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 499.2109069824219, 463.45703125)</val>
</matrix>
<points>
<val>[(28.486861756586336, 62.3125), (25.111328125, -14.58984375)]</val>
</points>
<head-connection>
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
</tail-connection>
</UML:GeneralizationItem>
<UML:Generalization id="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8">
<general>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
</general>
<presentation>
<reflist>
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
<specific>
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</specific>
</UML:Generalization>
<UML:Interface id="198a3108-f5be-11ef-9ec2-4cbb5880a0b8">
<name>
<val>MangaParser</val>
</name>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<package>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</package>
<presentation>
<reflist>
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
<supplierDependency>
<reflist>
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</supplierDependency>
</UML:Interface>
<UML:InterfaceItem id="198aace6-f5be-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 278.00391387939453, 232.92578125)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>105.0</val>
</width>
<height>
<val>80.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<show_attributes>
<val>0</val>
</show_attributes>
<show_operations>
<val>0</val>
</show_operations>
<subject>
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
</subject>
<folded>
<val>0</val>
</folded>
</UML:InterfaceItem>
<UML:InterfaceRealizationItem id="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<subject>
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 306.1445007324219, 270.0625)</val>
</matrix>
<points>
<val>[(55.866059373910275, 42.86328125), (164.5765002560883, 118.8046875)]</val>
</points>
<head-connection>
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
</tail-connection>
</UML:InterfaceRealizationItem>
<UML:InterfaceRealization id="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8">
<client>
<reflist>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
</reflist>
</client>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<presentation>
<reflist>
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
<supplier>
<reflist>
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</supplier>
</UML:InterfaceRealization>
<UML:GeneralizationItem id="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<subject>
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 436.2929382324219, 439.1913757324219)</val>
</matrix>
<points>
<val>[(20.37646032737257, 188.27737426757812), (18.488327026367188, 9.675811767578125)]</val>
</points>
<head-connection>
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
</tail-connection>
</UML:GeneralizationItem>
<UML:Generalization id="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8">
<general>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
</general>
<presentation>
<reflist>
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
<specific>
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</specific>
</UML:Generalization>
<UML:Class id="32081654-f5bf-11ef-9ec2-4cbb5880a0b8">
<clientDependency>
<reflist>
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</clientDependency>
<name>
<val>MangaParserWrapper</val>
</name>
<note>
<val></val>
</note>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<package>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</package>
<presentation>
<reflist>
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Class>
<UML:ClassItem id="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 128.5008992667698, 410.48990205860804)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>158.0</val>
</width>
<height>
<val>60.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<show_attributes>
<val>0</val>
</show_attributes>
<show_operations>
<val>0</val>
</show_operations>
<subject>
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:ClassItem>
<UML:InterfaceRealizationItem id="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<subject>
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 306.0585632324219, 249.69920349121094)</val>
</matrix>
<points>
<val>[(11.759223915218172, 63.22657775878906), (-98.55766396565207, 160.7906985673971)]</val>
</points>
<head-connection>
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:InterfaceRealizationItem>
<UML:InterfaceRealization id="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8">
<client>
<reflist>
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</client>
<owningPackage>
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
</owningPackage>
<presentation>
<reflist>
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
<supplier>
<reflist>
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</supplier>
</UML:InterfaceRealization>
<UML:Comment id="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8">
<body>
<val>Used for providing external api. Do not use this class directly</val>
</body>
<presentation>
<reflist>
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Comment>
<UML:CommentItem id="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 108.0561294327963, 550.1347579956054)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>183.21868896484375</val>
</width>
<height>
<val>91.23829650878906</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<subject>
<ref refid="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:CommentItem>
<UML:CommentLineItem id="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 549.205520203852, 278.05499559311954)</val>
</matrix>
<points>
<val>[(-349.5400462886338, 192.4349064654885), (-349.5400462886338, 272.0797624024858)]</val>
</points>
<tail-connection>
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:CommentLineItem>
<UML:Comment id="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8">
<annotatedElement>
<reflist>
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
</reflist>
</annotatedElement>
<body>
<val>Extend this class if your manga source provides standard limit-offset based lists (get manga list by offset)</val>
</body>
<presentation>
<reflist>
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Comment>
<UML:CommentItem id="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 673.0610499890082, 367.0515553989646)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>228.8028016098773</val>
</width>
<height>
<val>88.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<subject>
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:CommentItem>
<UML:Comment id="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8">
<annotatedElement>
<reflist>
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</annotatedElement>
<body>
<val>Extend this class if your manga source provides paged-based lists (get manga list by page number)</val>
</body>
<presentation>
<reflist>
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Comment>
<UML:CommentItem id="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 507.7539062499999)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>214.34368896484375</val>
</width>
<height>
<val>88.0</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<subject>
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:CommentItem>
<UML:Comment id="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8">
<annotatedElement>
<reflist>
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</annotatedElement>
<body>
<val>Extend this class if your manga source does not provide pagination (all manga provided in one list)</val>
</body>
<presentation>
<reflist>
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Comment>
<UML:CommentItem id="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 560.9671898788581)</val>
</matrix>
<top-left>
<val>(0.0, 58.00435704705592)</val>
</top-left>
<width>
<val>263.9307954323941</val>
</width>
<height>
<val>78.01706672440287</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<subject>
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:CommentItem>
<UML:CommentLineItem id="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 561.8951626340418, 549.6101338901756)</val>
</matrix>
<points>
<val>[(56.44167413272805, 7.038279316310902), (104.89726853244963, 8.304008355003589)]</val>
</points>
<head-connection>
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:CommentLineItem>
<UML:CommentLineItem id="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 559.3873501340418, 413.0007588901755)</val>
</matrix>
<points>
<val>[(3.7806186159582467, 0.0), (113.67369985496646, 1.6012844908540842)]</val>
</points>
<head-connection>
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:CommentLineItem>
<UML:CommentLineItem id="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 522.3600063840418, 652.6882588901756)</val>
</matrix>
<points>
<val>[(57.80796236595825, 5.29182139794003), (144.43242478244963, 5.657840086725969)]</val>
</points>
<head-connection>
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:CommentLineItem>
<general:Box id="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 375.05802564891326, 349.05453145170736)</val>
</matrix>
<top-left>
<val>(0.0, 3.15625)</val>
</top-left>
<width>
<val>590.6594026101285</val>
</width>
<height>
<val>368.44140625</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
</general:Box>
<UML:Comment id="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8">
<body>
<val>To create your own parser you have to extends one of these classes</val>
</body>
<presentation>
<reflist>
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
</reflist>
</presentation>
</UML:Comment>
<UML:CommentItem id="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 756.725301794198, 225.57697659840966)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>208.99212646484375</val>
</width>
<height>
<val>73.47482464883183</val>
</height>
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<subject>
<ref refid="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8"/>
</subject>
</UML:CommentItem>
<UML:CommentLineItem id="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8">
<diagram>
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
</diagram>
<horizontal>
<val>False</val>
</horizontal>
<orthogonal>
<val>False</val>
</orthogonal>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 943.6141683885666, 419.2772168262177)</val>
</matrix>
<points>
<val>[(-27.404961772030788, -67.06643537451032), (-27.404961772030788, -120.2254155789762)]</val>
</points>
<head-connection>
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
</head-connection>
<tail-connection>
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
</tail-connection>
</UML:CommentLineItem>
</model>
</gaphor>

BIN
docs/parser_classes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,2 +1,9 @@
# Following this blog:
# https://proandroiddev.com/how-we-reduced-our-gradle-build-times-by-over-80-51f2b6d6b05b
kotlin.code.style=official
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=4096m
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=4096m -XX:+UseParallelGC
org.gradle.vfs.watch=true
org.gradle.configureondemand=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.unsafe.configuration-cache=true

29
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,29 @@
[versions]
kotlin = "2.2.10"
ksp = "2.2.10-2.0.2"
coroutines = "1.10.2"
junit = "5.10.1"
okhttp = "5.1.0"
okio = "3.16.0"
json = "20240303"
androidx-collection = "1.5.0"
jsoup = "1.21.2"
quickjs = "1.1.0"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
[libraries]
ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
json = { module = "org.json:json", version.ref = "json" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
quickjs = { module = "io.webfolder:quickjs", version.ref = "quickjs" }

View File

@@ -1,5 +1,7 @@
#Wed Aug 27 01:56:37 ICT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,7 +0,0 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14'
}

View File

@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation(libs.ksp.symbol.processing.api)
}

View File

@@ -7,165 +7,180 @@ import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSVisitorVoid
import com.google.devtools.ksp.validate
import java.io.File
import java.io.Writer
import java.util.*
class ParserProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
private val options: Map<String, String>,
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
private val options: Map<String, String>,
) : SymbolProcessor {
private val availableLocales = Locale.getAvailableLocales().toSet()
private val sourceNamePattern = Regex("[A-Z_][A-Z0-9_]{3,}")
private val availableLocales = Locale.getAvailableLocales().toSet()
private val sourceNamePattern = Regex("[A-Z_][A-Z0-9_]{3,}")
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser")
val ret = symbols.filterNot { it.validate() }.toList()
if (!symbols.iterator().hasNext()) {
return ret
}
val dependencies = Dependencies.ALL_FILES
val factoryFile =
try {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = "org.koitharu.kotatsu.parsers",
fileName = "MangaParserFactory",
)
} catch (e: FileAlreadyExistsException) {
logger.warn(e.toString(), null)
null
}
val sourcesFile =
try {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = "org.koitharu.kotatsu.parsers.model",
fileName = "MangaSource",
)
} catch (e: FileAlreadyExistsException) {
logger.warn(e.toString(), null)
null
}
val totalCount = sourcesFile?.writer().use { sourcesWriter ->
factoryFile?.writer().use { factoryWriter ->
writeContent(sourcesWriter, factoryWriter, symbols)
}
}
writeSummary(totalCount)
return ret
}
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver
.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser")
val ret = symbols.filterNot { it.validate() }.toList()
if (!symbols.iterator().hasNext()) {
return ret
}
val dependencies = Dependencies.ALL_FILES
val factoryFile = try {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = "org.koitharu.kotatsu.parsers",
fileName = "MangaParserFactory",
)
} catch (e: FileAlreadyExistsException) {
logger.warn(e.toString(), null)
null
}
val sourcesFile = try {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = "org.koitharu.kotatsu.parsers.model",
fileName = "MangaSource",
)
} catch (e: FileAlreadyExistsException) {
logger.warn(e.toString(), null)
null
}
sourcesFile?.writer().use { sourcesWriter ->
factoryFile?.writer().use { factoryWriter ->
writeContent(sourcesWriter, factoryWriter, symbols)
}
}
return ret
}
private fun writeContent(
sourcesWriter: Writer?,
factoryWriter: Writer?,
symbols: Sequence<KSAnnotated>,
): Int {
if (sourcesWriter == null && factoryWriter == null) {
return 0
}
factoryWriter?.write(
"""
package org.koitharu.kotatsu.parsers
private fun writeContent(
sourcesWriter: Writer?,
factoryWriter: Writer?,
symbols: Sequence<KSAnnotated>,
) {
if (sourcesWriter == null && factoryWriter == null) {
return
}
factoryWriter?.write(
"""
package org.koitharu.kotatsu.parsers
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.core.MangaParserWrapper
internal fun MangaParserSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
import org.koitharu.kotatsu.parsers.model.MangaSource
@Suppress("DEPRECATION")
@InternalParsersApi
@Deprecated("", replaceWith = ReplaceWith("context.newParserInstance(this)"))
fun MangaSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
""".trimIndent(),
)
//language=kotlin
sourcesWriter?.write(
"""
package org.koitharu.kotatsu.parsers.model
enum class MangaSource(
val title: String,
val locale: String?,
val contentType: ContentType,
) {
LOCAL("Local", null, ContentType.OTHER),
)
sourcesWriter?.write(
"""
package org.koitharu.kotatsu.parsers.model
public enum class MangaParserSource(
public val title: String,
public val locale: String,
public val contentType: ContentType,
public val isBroken: Boolean,
): MangaSource {
""".trimIndent(),
)
)
val visitor = ParserVisitor(sourcesWriter, factoryWriter)
symbols
.filter { it is KSClassDeclaration && it.validate() }
.forEach { it.accept(visitor, Unit) }
val visitor = ParserVisitor(sourcesWriter, factoryWriter)
val totalCount = symbols
.filter { it is KSClassDeclaration && it.validate() }
.onEach { it.accept(visitor, Unit) }
.count()
factoryWriter?.write(
"""
MangaSource.LOCAL -> throw NotImplementedError("Local manga parser is not supported")
MangaSource.DUMMY -> throw NotImplementedError("Dummy manga parser cannot be instantiated")
}.also {
factoryWriter?.write(
"""
}.let {
require(it.source == this) {
"Cannot instantiate manga parser: ${'$'}name mapped to ${'$'}{it.source}"
}
MangaParserWrapper(it)
}
""".trimIndent(),
)
sourcesWriter?.write(
"""
DUMMY("Dummy", null, ContentType.OTHER),
)
sourcesWriter?.write(
"""
;
}
""".trimIndent(),
)
}
)
return totalCount
}
private inner class ParserVisitor(
private val sourcesWriter: Writer?,
private val factoryWriter: Writer?,
) : KSVisitorVoid() {
private fun writeSummary(totalCount: Int) {
val file = File(options["summaryOutputDir"] ?: return, "summary.yaml")
file.writeText("total: $totalCount")
}
private val titles = HashMap<String, String>()
private inner class ParserVisitor(
private val sourcesWriter: Writer?,
private val factoryWriter: Writer?,
) : KSVisitorVoid() {
private val titles = HashMap<String, String>()
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
if (classDeclaration.classKind != ClassKind.CLASS || classDeclaration.isAbstract()) {
logger.error("Only non-abstract can be annotated with @MangaSourceParser", classDeclaration)
}
val annotation = classDeclaration.annotations.single { it.shortName.asString() == "MangaSourceParser" }
val deprecation = classDeclaration.annotations.singleOrNull { it.shortName.asString() == "Deprecated" }
val name = annotation.arguments.single { it.name?.asString() == "name" }.value as String
val title = annotation.arguments.single { it.name?.asString() == "title" }.value as String
val locale = annotation.arguments.single { it.name?.asString() == "locale" }.value as String
val type = annotation.arguments.single { it.name?.asString() == "type" }.value
val localeString = if (locale.isEmpty()) "null" else "\"$locale\""
val localeObj = if (locale.isEmpty()) null else Locale(locale)
val localeTitle = localeObj?.getDisplayLanguage(localeObj)
if (localeObj != null && localeObj !in availableLocales) {
logger.error("Manga source $name has wrong locale: $localeTitle")
}
override fun visitClassDeclaration(
classDeclaration: KSClassDeclaration,
data: Unit,
) {
if (classDeclaration.classKind != ClassKind.CLASS || classDeclaration.isAbstract()) {
logger.error("Only non-abstract can be annotated with @MangaSourceParser", classDeclaration)
}
val annotation = classDeclaration.annotations.single { it.shortName.asString() == "MangaSourceParser" }
val deprecation = classDeclaration.annotations.singleOrNull { it.shortName.asString() == "Deprecated" }
val isBroken = classDeclaration.annotations.any { it.shortName.asString() == "Broken" }
val name = annotation.arguments.single { it.name?.asString() == "name" }.value as String
val title = annotation.arguments.single { it.name?.asString() == "title" }.value as String
val locale = annotation.arguments.single { it.name?.asString() == "locale" }.value as String
val type = annotation.arguments.single { it.name?.asString() == "type" }.value
val localeString = "\"$locale\""
val localeObj = if (locale.isEmpty()) null else Locale(locale)
val localeTitle = localeObj?.getDisplayLanguage(localeObj)
if (localeObj != null && localeObj !in availableLocales) {
logger.error("Manga source $name has wrong locale: $localeTitle")
}
if (!sourceNamePattern.matches(name)) {
logger.error("Manga source name must be uppercase: $name")
}
if (!sourceNamePattern.matches(name)) {
logger.error("Manga source name must be uppercase: $name")
}
val constructor = classDeclaration.primaryConstructor
if (constructor == null || constructor.parameters.count { !it.hasDefault } != 1) {
logger.error(
"Class with @MangaSourceParser must have a primary constructor with one parameter",
classDeclaration,
)
}
val className = checkNotNull(classDeclaration.qualifiedName?.asString()) { "Class name is null" }
val constructor = classDeclaration.primaryConstructor
if (constructor == null || constructor.parameters.count { !it.hasDefault } != 1) {
logger.error(
"Class with @MangaSourceParser must have a primary constructor with one parameter",
classDeclaration,
)
}
val className = checkNotNull(classDeclaration.qualifiedName?.asString()) { "Class name is null" }
val prevTitleClass = titles.put(title, className)
if (prevTitleClass != null) {
logger.warn("Source title duplication: \"$title\" is assigned to both $prevTitleClass and $className")
}
val prevTitleClass = titles.put(title, className)
if (prevTitleClass != null) {
logger.warn("Source title duplication: \"$title\" is assigned to both $prevTitleClass and $className")
}
factoryWriter?.write("\tMangaSource.$name -> $className(context)\n")
val deprecationString = if (deprecation != null) {
val reason = deprecation.arguments
.find { it.name?.asString() == "message" }?.value?.toString() ?: "Unknown reason"
"@Deprecated(\"$reason\") "
} else ""
val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
sourcesWriter?.write("\t$deprecationString$name(\"$title\", $localeString$localeComment, ContentType.$type),\n")
}
}
factoryWriter?.write("\tMangaParserSource.$name -> $className(context)\n")
val deprecationString =
if (deprecation != null) {
val reason =
deprecation.arguments
.find { it.name?.asString() == "message" }
?.value
?.toString() ?: "Unknown reason"
"@Deprecated(\"$reason\") "
} else {
""
}
val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
sourcesWriter?.write(
"\t$deprecationString$name(\"$title\", $localeString$localeComment, $type, $isBroken),\n",
)
}
}
}

View File

@@ -1,19 +0,0 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = 'kotatsu-parsers'
include 'kotatsu-parsers-ksp'

18
settings.gradle.kts Normal file
View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "kotatsu-parsers"
include("kotatsu-parsers-ksp")

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.parsers
/**
* Annotate [MangaParser] implementation to mark this parser as broken instead of removing it
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
internal annotation class Broken(
/**
* Reason why this parser is broken
*/
val message: String = "",
)

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.parsers
public object ErrorMessages {
public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source"
public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source"
public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String =
"Multiple Content ratings are not supported by this source"
public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String =
"Multiple Content types are not supported by this source"
public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String =
"Multiple Demographics are not supported by this source"
public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String =
"Filtering by both genres and locale is not supported by this source"
public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String =
"Filtering by both genres and states is not supported by this source"
public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source"
}

View File

@@ -11,4 +11,4 @@ package org.koitharu.kotatsu.parsers
@SinceKotlin("1.3")
@RequiresOptIn
@MustBeDocumented
annotation class InternalParsersApi
public annotation class InternalParsersApi

View File

@@ -1,32 +1,78 @@
package org.koitharu.kotatsu.parsers
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Response
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.LinkResolver
import java.util.*
abstract class MangaLoaderContext {
public abstract class MangaLoaderContext {
abstract val httpClient: OkHttpClient
public abstract val httpClient: OkHttpClient
abstract val cookieJar: CookieJar
public abstract val cookieJar: CookieJar
@Suppress("DEPRECATION")
fun newParserInstance(source: MangaSource): MangaParser = source.newParser(this)
public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
public fun newLinkResolver(link: HttpUrl): LinkResolver = LinkResolver(this, link)
open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
public fun newLinkResolver(link: String): LinkResolver = newLinkResolver(link.toHttpUrl())
open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
public open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
/**
* Execute JavaScript code and return result
* @param script JavaScript source code
* @return execution result as string, may be null
*/
abstract suspend fun evaluateJs(script: String): String?
@Deprecated("Provide a base url")
public abstract suspend fun evaluateJs(script: String): String?
abstract fun getConfig(source: MangaSource): MangaSourceConfig
/**
* Execute JavaScript code and return result
* @param script JavaScript source code
* @param baseUrl url of page script will be executed in context of
* @return execution result as string, may be null
*/
public abstract suspend fun evaluateJs(baseUrl: String, script: String): String?
/**
* Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization)
*/
public open fun requestBrowserAction(parser: MangaParser, url: String): Nothing {
throw UnsupportedOperationException("Browser is not available")
}
public abstract fun getConfig(source: MangaSource): MangaSourceConfig
public abstract fun getDefaultUserAgent(): String
/**
* Helper function to be used in an interceptor
* to descramble images
* @param response Image response
* @param redraw lambda function to implement descrambling logic
*/
public abstract fun redrawImageResponse(
response: Response,
redraw: (image: Bitmap) -> Bitmap,
): Response
/**
* create a new empty Bitmap with given dimensions
*/
public abstract fun createBitmap(
width: Int,
height: Int,
): Bitmap
}

View File

@@ -1,203 +1,88 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.network.WebClient
import org.koitharu.kotatsu.parsers.util.FaviconParser
import org.koitharu.kotatsu.parsers.util.RelatedMangaFinder
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.util.LinkResolver
import org.koitharu.kotatsu.parsers.util.convertToMangaSearchQuery
import org.koitharu.kotatsu.parsers.util.toMangaListFilterCapabilities
import java.util.*
abstract class MangaParser @InternalParsersApi constructor(
@property:InternalParsersApi val context: MangaLoaderContext,
val source: MangaSource,
) {
public interface MangaParser : Interceptor {
public val source: MangaParserSource
/**
* Supported [SortOrder] variants. Must not be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
abstract val availableSortOrders: Set<SortOrder>
public val availableSortOrders: Set<SortOrder>
/**
* Supported [MangaState] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
open val availableStates: Set<MangaState>
get() = emptySet()
@Deprecated("Too complex. Use filterCapabilities instead")
public val searchQueryCapabilities: MangaSearchQueryCapabilities
/**
* Whether parser supports filtering by more than one tag
*/
open val isMultipleTagsSupported: Boolean = true
public val filterCapabilities: MangaListFilterCapabilities
@Deprecated(
message = "Use availableSortOrders instead",
replaceWith = ReplaceWith("availableSortOrders"),
)
val sortOrders: Set<SortOrder>
get() = availableSortOrders
public val config: MangaSourceConfig
val config by lazy { context.getConfig(source) }
open val sourceLocale: Locale
get() = source.locale?.let { Locale(it) } ?: Locale.ROOT
val isNsfwSource = source.contentType == ContentType.HENTAI
public val authorizationProvider: MangaParserAuthProvider?
get() = this as? MangaParserAuthProvider
/**
* Provide default domain and available alternatives, if any.
*
* Never hardcode domain in requests, use [domain] instead.
*/
@InternalParsersApi
abstract val configKeyDomain: ConfigKey.Domain
public val configKeyDomain: ConfigKey.Domain
open val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
public val domain: String
/**
* Used as fallback if value of `sortOrder` passed to [getList] is null
*/
protected open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }
}
@Deprecated("Too complex. Use getList with filter instead")
public suspend fun getList(query: MangaSearchQuery): List<Manga>
@JvmField
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
/**
* Parse list of manga by specified criteria
*
* @param offset starting from 0 and used for pagination.
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
* @param query search query, may be null or empty if no search needed
* @param tags genres for filtering, values from [getAvailableTags] and [Manga.tags]. May be null or empty
* @param sortOrder one of [availableSortOrders] or null for default value
*/
@JvmSynthetic
@InternalParsersApi
@Deprecated(
"Use getList with filter instead",
replaceWith = ReplaceWith("getList(offset, filter)"),
)
abstract suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga>
/**
* Parse list of manga with search by text query
*
* @param offset starting from 0 and used for pagination.
* @param query search query
*/
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, MangaListFilter.Search(query))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(offset: Int, query: String): List<Manga> {
return getList(offset, MangaListFilter.Search(query))
}
/**
* Parse list of manga by specified criteria
*
* @param offset starting from 0 and used for pagination.
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
* @param tags genres for filtering, values from [getAvailableTags] and [Manga.tags]. May be null or empty
* @param sortOrder one of [availableSortOrders] or null for default value
*/
@Deprecated(
"Use getList with filter instead",
ReplaceWith(
"getList(offset, MangaListFilter.Advanced(sortOrder, tags, null, emptySet()))",
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
return getList(
offset,
MangaListFilter.Advanced(sortOrder ?: defaultSortOrder, tags.orEmpty(), null, emptySet()),
)
}
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)
}
}
public suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
/**
* Parse details for [Manga]: chapters list, description, large cover, etc.
* Must return the same manga, may change any fields excepts id, url and source
* @see Manga.copy
*/
abstract suspend fun getDetails(manga: Manga): Manga
public suspend fun getDetails(manga: Manga): Manga
/**
* Parse pages list for specified chapter.
* @see MangaPage for details
*/
abstract suspend fun getPages(chapter: MangaChapter): List<MangaPage>
public suspend fun getPages(chapter: MangaChapter): List<MangaPage>
/**
* Fetch direct link to the page image.
*/
open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
public suspend fun getPageUrl(page: MangaPage): String
/**
* Fetch available tags (genres) for source
*/
abstract suspend fun getAvailableTags(): Set<MangaTag>
/**
* Fetch available locales for multilingual sources
*/
open suspend fun getAvailableLocales(): Set<Locale> = emptySet()
@Deprecated(
message = "Use getAvailableTags instead",
replaceWith = ReplaceWith("getAvailableTags()"),
)
suspend fun getTags(): Set<MangaTag> = getAvailableTags()
public suspend fun getFilterOptions(): MangaListFilterOptions
/**
* Parse favicons from the main page of the source`s website
*/
open suspend fun getFavicons(): Favicons {
return FaviconParser(webClient, domain).parseFavicons()
}
public suspend fun getFavicons(): Favicons
@CallSuper
open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
keys.add(configKeyDomain)
}
public fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>)
open suspend fun getRelatedManga(seed: Manga): List<Manga> {
return RelatedMangaFinder(listOf(this)).invoke(seed)
}
public suspend fun getRelatedManga(seed: Manga): List<Manga>
protected fun getParser(source: MangaSource) = if (this.source == source) {
this
} else {
context.newParserInstance(source)
}
public fun getRequestHeaders(): Headers
/**
* Return [Manga] object by web link to it
* @see [Manga.publicUrl]
*/
@InternalParsersApi
public suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga?
}

View File

@@ -6,19 +6,19 @@ import org.koitharu.kotatsu.parsers.exception.ParseException
/**
* Implement this in your parser for authorization support
*/
interface MangaParserAuthProvider {
public interface MangaParserAuthProvider {
/**
* Return link to the login page, which will be opened in browser.
* Must be an absolute url
*/
val authUrl: String
public val authUrl: String
/**
* Quick check if user is logged in.
* In most case you should check for cookies in [MangaLoaderContext.cookieJar].
*/
val isAuthorized: Boolean
public suspend fun isAuthorized(): Boolean
/**
* Fetch and return current user`s name or login.
@@ -26,5 +26,5 @@ interface MangaParserAuthProvider {
* @throws [AuthRequiredException] if user is not logged in or authorization is expired
* @throws [ParseException] on parsing error
*/
suspend fun getUsername(): String
}
public suspend fun getUsername(): String
}

View File

@@ -6,7 +6,8 @@ import org.koitharu.kotatsu.parsers.model.ContentType
* Annotate each [MangaParser] implementation with this annotation, used by codegen
*/
@Target(AnnotationTarget.CLASS)
annotation class MangaSourceParser(
@Retention(AnnotationRetention.SOURCE)
internal annotation class MangaSourceParser(
/**
* Name of manga source. Used as an Enum value, must be UPPER_CASE and unique.
*/

View File

@@ -1,55 +0,0 @@
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.util.Paginator
@InternalParsersApi
abstract class PagedMangaParser(
context: MangaLoaderContext,
source: MangaSource,
@RestrictTo(RestrictTo.Scope.TESTS) @JvmField internal val pageSize: Int,
searchPageSize: Int = pageSize,
) : MangaParser(context, source) {
@JvmField
protected val paginator = Paginator(pageSize)
@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)
}
@InternalParsersApi
@Deprecated("You should use getListPage for PagedMangaParser", level = DeprecationLevel.HIDDEN)
final override suspend fun getList(
offset: Int,
query: String?,
tags: 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>
private suspend fun getList(
paginator: Paginator,
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val page = paginator.getPage(offset)
val list = getListPage(page, query, tags, sortOrder)
paginator.onListReceived(offset, page, list.size)
return list
}
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.parsers.bitmap
public interface Bitmap {
public val width: Int
public val height: Int
public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect)
}

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.parsers.bitmap
public data class Rect(
val left: Int = 0,
val top: Int = 0,
val right: Int = 0,
val bottom: Int = 0,
) {
val width: Int
get() = right - left
val height: Int
get() = bottom - top
}

View File

@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.parsers.config
sealed class ConfigKey<T>(
@JvmField val key: String,
public sealed class ConfigKey<T>(
@JvmField public val key: String,
) {
abstract val defaultValue: T
public abstract val defaultValue: T
class Domain(
@JvmField @JvmSuppressWildcards vararg val presetValues: String,
public class Domain(
@JvmField @JvmSuppressWildcards public vararg val presetValues: String,
) : ConfigKey<String>("domain") {
init {
@@ -18,11 +18,20 @@ sealed class ConfigKey<T>(
get() = presetValues.first()
}
class ShowSuspiciousContent(
public class ShowSuspiciousContent(
override val defaultValue: Boolean,
) : ConfigKey<Boolean>("show_suspicious")
class UserAgent(
public class UserAgent(
override val defaultValue: String,
) : ConfigKey<String>("user_agent")
public class SplitByTranslations(
override val defaultValue: Boolean,
) : ConfigKey<Boolean>("split_translations")
public class PreferredImageServer(
public val presetValues: Map<String?, String?>,
override val defaultValue: String?,
) : ConfigKey<String?>("img_server")
}

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.parsers.config
interface MangaSourceConfig {
public interface MangaSourceConfig {
operator fun <T> get(key: ConfigKey<T>): T
}
public operator fun <T> get(key: ConfigKey<T>): T
}

View File

@@ -0,0 +1,105 @@
package org.koitharu.kotatsu.parsers.core
import androidx.annotation.CallSuper
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
import org.koitharu.kotatsu.parsers.network.WebClient
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@Suppress("OVERRIDE_DEPRECATION")
@InternalParsersApi
public abstract class AbstractMangaParser @InternalParsersApi constructor(
@property:InternalParsersApi public val context: MangaLoaderContext,
public final override val source: MangaParserSource,
) : MangaParser {
public final override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = filterCapabilities.toMangaSearchQueryCapabilities()
public override val config: MangaSourceConfig by lazy { context.getConfig(source) }
public open val sourceLocale: Locale
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
protected val sourceContentRating: ContentRating?
get() = if (source.contentType == ContentType.HENTAI) {
ContentRating.ADULT
} else {
null
}
protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", config[userAgentKey])
.build()
/**
* Used as fallback if value of `order` passed to [getList] is null
*/
public open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }
}
final override val domain: String
get() = config[configKeyDomain]
@JvmField
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
/**
* Search list of manga by specified searchQuery
*
* @param query searchQuery
*/
public final override suspend fun getList(query: MangaSearchQuery): List<Manga> = getList(
offset = query.offset,
order = query.order ?: defaultSortOrder,
filter = convertToMangaListFilter(query),
)
/**
* Fetch direct link to the page image.
*/
public override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
/**
* Parse favicons from the main page of the source`s website
*/
public override suspend fun getFavicons(): Favicons {
return FaviconParser(webClient, domain).parseFavicons()
}
@CallSuper
public override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
keys.add(configKeyDomain)
}
public override suspend fun getRelatedManga(seed: Manga): List<Manga> {
return RelatedMangaFinder(listOf(this)).invoke(seed)
}
/**
* Return [Manga] object by web link to it
* @see [Manga.publicUrl]
*/
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
}

View File

@@ -0,0 +1,94 @@
package org.koitharu.kotatsu.parsers.core
import androidx.annotation.CallSuper
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
import org.koitharu.kotatsu.parsers.network.WebClient
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@Deprecated("Too complex. Use AbstractMangaParser instead")
internal abstract class FlexibleMangaParser @InternalParsersApi constructor(
@property:InternalParsersApi val context: MangaLoaderContext,
final override val source: MangaParserSource,
) : MangaParser {
override val config: MangaSourceConfig by lazy { context.getConfig(source) }
open val sourceLocale: Locale
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
final override val filterCapabilities: MangaListFilterCapabilities
get() = searchQueryCapabilities.toMangaListFilterCapabilities()
protected val sourceContentRating: ContentRating?
get() = if (source.contentType == ContentType.HENTAI) {
ContentRating.ADULT
} else {
null
}
final override val domain: String
get() = config[configKeyDomain]
@Deprecated("Override intercept() instead")
override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", config[userAgentKey])
.build()
/**
* Used as fallback if value of `order` passed to [getList] is null
*/
open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }
}
@JvmField
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
/**
* Fetch direct link to the page image.
*/
override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return getList(convertToMangaSearchQuery(offset, order, filter))
}
/**
* Parse favicons from the main page of the source`s website
*/
override suspend fun getFavicons(): Favicons {
return FaviconParser(webClient, domain).parseFavicons()
}
@CallSuper
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
keys.add(configKeyDomain)
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
return RelatedMangaFinder(listOf(this)).invoke(seed)
}
/**
* Return [Manga] object by web link to it
* @see [Manga.publicUrl]
*/
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
}

View File

@@ -0,0 +1,60 @@
package org.koitharu.kotatsu.parsers.core
import androidx.annotation.VisibleForTesting
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.SearchableField
import org.koitharu.kotatsu.parsers.util.Paginator
@Deprecated("Too complex. Use PagedMangaParser instead")
internal abstract class FlexiblePagedMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
searchPageSize: Int = pageSize,
) : FlexibleMangaParser(context, source) {
@JvmField
protected val paginator: Paginator = Paginator(pageSize)
@JvmField
protected val searchPaginator: Paginator = Paginator(searchPageSize)
final override suspend fun getList(query: MangaSearchQuery): List<Manga> {
var containTitleNameCriteria = false
query.criteria.forEach {
if (it.field == SearchableField.TITLE_NAME) {
containTitleNameCriteria = true
}
}
return searchManga(
paginator = if (containTitleNameCriteria) {
paginator
} else {
searchPaginator
},
query = query,
)
}
public abstract suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga>
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
paginator.firstPage = firstPage
searchPaginator.firstPage = firstPageForSearch
}
private suspend fun searchManga(
paginator: Paginator,
query: MangaSearchQuery,
): List<Manga> {
val offset: Int = query.offset
val page = paginator.getPage(offset)
val list = getListPage(query, page)
paginator.onListReceived(offset, page, list.size)
return list
}
}

View File

@@ -0,0 +1,77 @@
package org.koitharu.kotatsu.parsers.core
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.util.mergeWith
internal class MangaParserWrapper(
private val delegate: MangaParser,
) : MangaParser by delegate {
override val authorizationProvider: MangaParserAuthProvider?
get() = delegate as? MangaParserAuthProvider
@Deprecated("Too complex. Use getList with filter instead")
override suspend fun getList(query: MangaSearchQuery): List<Manga> = withContext(Dispatchers.Default) {
if (!query.skipValidation) {
searchQueryCapabilities.validate(query)
}
delegate.getList(query)
}
override suspend fun getList(
offset: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> = withContext(Dispatchers.Default) {
delegate.getList(offset, order, filter)
}
override suspend fun getDetails(manga: Manga): Manga = withContext(Dispatchers.Default) {
delegate.getDetails(manga)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = withContext(Dispatchers.Default) {
delegate.getPages(chapter)
}
override suspend fun getPageUrl(page: MangaPage): String = withContext(Dispatchers.Default) {
delegate.getPageUrl(page)
}
override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.Default) {
delegate.getFilterOptions()
}
override suspend fun getFavicons(): Favicons = withContext(Dispatchers.Default) {
delegate.getFavicons()
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> = withContext(Dispatchers.Default) {
delegate.getRelatedManga(seed)
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val headers = request.headers.newBuilder()
.mergeWith(delegate.getRequestHeaders(), replaceExisting = false)
.build()
val newRequest = request.newBuilder().headers(headers).build()
return delegate.intercept(ProxyChain(chain, newRequest))
}
private class ProxyChain(
private val delegate: Interceptor.Chain,
private val request: Request,
) : Interceptor.Chain by delegate {
override fun request(): Request = request
}
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.parsers.core
import androidx.annotation.VisibleForTesting
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.Paginator
@InternalParsersApi
public abstract class PagedMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
searchPageSize: Int = pageSize,
) : AbstractMangaParser(context, source) {
@JvmField
protected val paginator: Paginator = Paginator(pageSize)
@JvmField
protected val searchPaginator: Paginator = Paginator(searchPageSize)
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return getList(
paginator = if (filter.query.isNullOrEmpty()) {
paginator
} else {
searchPaginator
},
offset = offset,
order = order,
filter = filter,
)
}
public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
paginator.firstPage = firstPage
searchPaginator.firstPage = firstPageForSearch
}
private suspend fun getList(
paginator: Paginator,
offset: Int,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val page = paginator.getPage(offset)
val list = getListPage(page, order, filter)
paginator.onListReceived(offset, page, list.size)
return list
}
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.parsers.core
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
@InternalParsersApi
public abstract class SinglePageMangaParser(
context: MangaLoaderContext,
source: MangaParserSource,
) : AbstractMangaParser(context, source) {
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
if (offset > 0) {
return emptyList()
}
return getList(order, filter)
}
public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga>
}

View File

@@ -1,12 +1,13 @@
package org.koitharu.kotatsu.parsers.exception
import okio.IOException
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.model.MangaSource
/**
* Authorization is required for access to the requested content
*/
class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
val source: MangaSource,
public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
public val source: MangaSource,
cause: Throwable? = null,
) : RuntimeException("Authorization required", cause)
) : IOException("Authorization required", cause)

View File

@@ -1,3 +1,3 @@
package org.koitharu.kotatsu.parsers.exception
class ContentUnavailableException(message: String) : RuntimeException(message)
public class ContentUnavailableException(message: String) : RuntimeException(message)

View File

@@ -2,14 +2,15 @@ package org.koitharu.kotatsu.parsers.exception
import okio.IOException
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
class GraphQLException(private val errors: JSONArray) : IOException() {
public class GraphQLException @InternalParsersApi constructor(errors: JSONArray) : IOException() {
val messages = errors.mapJSON {
public val messages: List<String> = errors.mapJSONNotNull {
it.getString("message")
}
override val message: String
get() = messages.joinToString("\n")
}
}

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.parsers.exception
import org.jsoup.HttpStatusException
import java.net.HttpURLConnection
class NotFoundException(
public class NotFoundException(
message: String,
url: String,
) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url)
) : HttpStatusException(message, HttpURLConnection.HTTP_NOT_FOUND, url)

View File

@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.parsers.exception
import org.koitharu.kotatsu.parsers.InternalParsersApi
class ParseException @InternalParsersApi @JvmOverloads constructor(
val shortMessage: String?,
val url: String,
public class ParseException @InternalParsersApi @JvmOverloads constructor(
public val shortMessage: String?,
public val url: String,
cause: Throwable? = null,
) : RuntimeException("$shortMessage at $url", cause)
) : RuntimeException("$shortMessage at $url", cause)

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.parsers.exception
import okio.IOException
import java.time.Instant
import java.time.temporal.ChronoUnit
public class TooManyRequestExceptions(
public val url: String,
retryAfter: Long,
) : IOException("Too man requests") {
public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) {
Instant.now().plusMillis(retryAfter)
} else {
null
}
public fun getRetryDelay(): Long {
if (retryAt == null) {
return -1L
}
return Instant.now().until(retryAt, ChronoUnit.MILLIS).coerceAtLeast(0L)
}
override val message: String?
get() = if (retryAt != null) {
"${super.message}, retry at $retryAt"
} else {
super.message
}
}

View File

@@ -2,4 +2,10 @@
package org.koitharu.kotatsu.parsers.model
const val RATING_UNKNOWN = -1f
public const val RATING_UNKNOWN: Float = -1f
public const val YEAR_UNKNOWN: Int = 0
public const val YEAR_MIN: Int = 1900
public const val YEAR_MAX: Int = 2099

View File

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

View File

@@ -1,12 +1,16 @@
package org.koitharu.kotatsu.parsers.model
enum class ContentType {
public enum class ContentType {
/**
* Standard manga, manhua, webtoons, etc
*/
MANGA,
MANHWA,
MANHUA,
/**
* Use this if the source provides mostly nsfw content.
*/
@@ -17,8 +21,16 @@ enum class ContentType {
*/
COMICS,
NOVEL,
/**
* Use this type if no other suits your needs. For example, for an indie manga
*/
ONE_SHOT,
DOUJINSHI,
IMAGE_SET,
ARTIST_CG,
GAME_CG,
OTHER,
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.model
public enum class Demographic {
SHOUNEN,
SHOUJO,
SEINEN,
JOSEI,
KODOMO,
NONE,
}

View File

@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.parsers.model
import okhttp3.HttpUrl.Companion.toHttpUrl
class Favicon internal constructor(
@JvmField val url: String,
@JvmField val size: Int,
public data class Favicon(
@JvmField public val url: String,
@JvmField public val size: Int,
@JvmField internal val rel: String?,
) : Comparable<Favicon> {
@JvmField
val type: String = url.toHttpUrl().pathSegments.last()
public val type: String = url.toHttpUrl().pathSegments.last()
.substringAfterLast('.', "").lowercase()
override fun compareTo(other: Favicon): Int {
@@ -20,30 +20,6 @@ class Favicon internal constructor(
return relWeightOf(rel).compareTo(relWeightOf(other.rel))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Favicon
if (url != other.url) return false
if (size != other.size) return false
if (rel != other.rel) return false
return true
}
override fun hashCode(): Int {
var result = url.hashCode()
result = 31 * result + size
result = 31 * result + rel.hashCode()
return result
}
override fun toString(): String {
return "Favicon(size=$size, type='$type', rel='$rel', url='$url')"
}
private fun relWeightOf(rel: String?) = when (rel) {
"apple-touch-icon" -> 1 // Prefer apple-touch-icon because it has a better quality
"mask-icon" -> -1

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.parsers.model
class Favicons internal constructor(
public class Favicons(
favicons: Collection<Favicon>,
@JvmField val referer: String,
@JvmField public val referer: String?,
) : Collection<Favicon> {
private val icons = favicons.sortedDescending()
@@ -18,7 +18,7 @@ class Favicons internal constructor(
override fun iterator(): Iterator<Favicon> = icons.iterator()
operator fun minus(victim: Favicon): Favicons = Favicons(
public operator fun minus(victim: Favicon): Favicons = Favicons(
favicons = icons.filterNot { it == victim },
referer = referer,
)
@@ -30,7 +30,7 @@ class Favicons internal constructor(
* @param types supported file types, e.g. png, svg, ico. May be null but not empty
*/
@JvmOverloads
fun find(size: Int, types: Set<String>? = null): Favicon? {
public fun find(size: Int, types: Set<String>? = null): Favicon? {
if (icons.isEmpty()) {
return null
}
@@ -47,4 +47,13 @@ class Favicons internal constructor(
}
return result
}
public companion object {
@JvmStatic
public val EMPTY: Favicons = Favicons(emptySet(), null)
@JvmStatic
public fun single(url: String): Favicons = Favicons(setOf(Favicon(url, 0, null)), null)
}
}

View File

@@ -1,162 +1,203 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
import androidx.collection.ArrayMap
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
class Manga(
public data class Manga(
/**
* Unique identifier for manga
*/
@JvmField val id: Long,
@JvmField public val id: Long,
/**
* Manga title, human-readable
*/
@JvmField val title: String,
@JvmField public val title: String,
/**
* Alternative title (for example on other language), may be null
* Alternative titles (for example on other language), may be empty
*/
@JvmField val altTitle: String?,
@JvmField public val altTitles: Set<String>,
/**
* Relative url to manga (**without** a domain) or any other uri.
* Used principally in parsers
*/
@JvmField val url: String,
@JvmField public val url: String,
/**
* Absolute url to manga, must be ready to open in browser
*/
@JvmField val publicUrl: String,
@JvmField public val publicUrl: String,
/**
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
* @see hasRating
*/
@JvmField val rating: Float,
@JvmField public val rating: Float,
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
@JvmField val isNsfw: Boolean,
@JvmField public val contentRating: ContentRating?,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
@JvmField val coverUrl: String,
@JvmField public val coverUrl: String?,
/**
* Tags (genres) of the manga
*/
@JvmField val tags: Set<MangaTag>,
@JvmField public val tags: Set<MangaTag>,
/**
* Manga status (ongoing, finished) or null if unknown
*/
@JvmField val state: MangaState?,
@JvmField public val state: MangaState?,
/**
* Author of the manga, may be null
* Authors of the manga
*/
@JvmField val author: String?,
@JvmField public val authors: Set<String>,
/**
* Large cover url (absolute), null if is no large cover
* @see coverUrl
*/
@JvmField val largeCoverUrl: String? = null,
@JvmField public val largeCoverUrl: String? = null,
/**
* Manga description, may be html or null
*/
@JvmField val description: String? = null,
@JvmField public val description: String? = null,
/**
* List of chapters
*/
@JvmField val chapters: List<MangaChapter>? = null,
@JvmField public val chapters: List<MangaChapter>? = null,
/**
* Manga source
*/
@JvmField val source: MangaSource,
@JvmField public val source: MangaSource,
) {
@Deprecated("Use other constructor")
public constructor(
/**
* Unique identifier for manga
*/
id: Long,
/**
* Manga title, human-readable
*/
title: String,
/**
* Alternative title (for example on other language), may be null
*/
altTitle: String?,
/**
* Relative url to manga (**without** a domain) or any other uri.
* Used principally in parsers
*/
url: String,
/**
* Absolute url to manga, must be ready to open in browser
*/
publicUrl: String,
/**
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
* @see hasRating
*/
rating: Float,
/**
* Indicates that manga may contain sensitive information (18+, NSFW)
*/
isNsfw: Boolean,
/**
* Absolute link to the cover
* @see largeCoverUrl
*/
coverUrl: String?,
/**
* Tags (genres) of the manga
*/
tags: Set<MangaTag>,
/**
* Manga status (ongoing, finished) or null if unknown
*/
state: MangaState?,
/**
* Authors of the manga
*/
author: String?,
/**
* Large cover url (absolute), null if is no large cover
* @see coverUrl
*/
largeCoverUrl: String? = null,
/**
* Manga description, may be html or null
*/
description: String? = null,
/**
* List of chapters
*/
chapters: List<MangaChapter>? = null,
/**
* Manga source
*/
source: MangaSource,
) : this(
id = id,
title = title,
altTitles = setOfNotNull(altTitle?.nullIfEmpty()),
url = url,
publicUrl = publicUrl,
rating = rating,
contentRating = if (isNsfw) ContentRating.ADULT else null,
coverUrl = coverUrl?.nullIfEmpty(),
tags = tags,
state = state,
authors = setOfNotNull(author),
largeCoverUrl = largeCoverUrl?.nullIfEmpty(),
description = description?.nullIfEmpty(),
chapters = chapters,
source = source,
)
/**
* Author of the manga, may be null
*/
@Deprecated("Please use authors")
public val author: String?
get() = authors.firstOrNull()
/**
* Alternative title (for example on other language), may be null
*/
@Deprecated("Please use altTitles")
public val altTitle: String?
get() = altTitles.firstOrNull()
/**
* Return if manga has a specified rating
* @see rating
*/
val hasRating: Boolean
public val hasRating: Boolean
get() = rating > 0f && rating <= 1f
fun getChapters(branch: String?): List<MangaChapter>? {
return chapters?.filter { x -> x.branch == branch }
@Deprecated("Use contentRating instead", ReplaceWith("contentRating == ContentRating.ADULT"))
public val isNsfw: Boolean
get() = contentRating == ContentRating.ADULT
public fun getChapters(branch: String?): List<MangaChapter> {
return chapters?.filter { x -> x.branch == branch }.orEmpty()
}
@InternalParsersApi
fun copy(
title: String = this.title,
altTitle: String? = this.altTitle,
publicUrl: String = this.publicUrl,
rating: Float = this.rating,
isNsfw: Boolean = this.isNsfw,
coverUrl: String = this.coverUrl,
tags: Set<MangaTag> = this.tags,
state: MangaState? = this.state,
author: String? = this.author,
largeCoverUrl: String? = this.largeCoverUrl,
description: String? = this.description,
chapters: List<MangaChapter>? = this.chapters,
) = Manga(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
chapters = chapters,
source = source,
)
public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
public fun requireChapterById(id: Long): MangaChapter = findChapterById(id)
?: throw NoSuchElementException("Chapter with id $id not found")
other as Manga
if (id != other.id) return false
if (title != other.title) return false
if (altTitle != other.altTitle) return false
if (url != other.url) return false
if (publicUrl != other.publicUrl) return false
if (rating != other.rating) return false
if (isNsfw != other.isNsfw) return false
if (coverUrl != other.coverUrl) return false
if (tags != other.tags) return false
if (state != other.state) return false
if (author != other.author) return false
if (largeCoverUrl != other.largeCoverUrl) return false
if (description != other.description) return false
if (chapters != other.chapters) return false
if (source != other.source) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + (altTitle?.hashCode() ?: 0)
result = 31 * result + url.hashCode()
result = 31 * result + publicUrl.hashCode()
result = 31 * result + rating.hashCode()
result = 31 * result + isNsfw.hashCode()
result = 31 * result + coverUrl.hashCode()
result = 31 * result + tags.hashCode()
result = 31 * result + (state?.hashCode() ?: 0)
result = 31 * result + (author?.hashCode() ?: 0)
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
result = 31 * result + (description?.hashCode() ?: 0)
result = 31 * result + (chapters?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
public fun getBranches(): Map<String?, Int> {
if (chapters.isNullOrEmpty()) {
return emptyMap()
}
val result = ArrayMap<String?, Int>()
chapters.forEach {
val key = it.branch
result[key] = result.getOrDefault(key, 0) + 1
}
return result
}
override fun toString(): String {
return "Manga($id - \"$title\" [$url] - $source)"
}
}

View File

@@ -1,74 +1,65 @@
package org.koitharu.kotatsu.parsers.model
class MangaChapter(
import org.koitharu.kotatsu.parsers.util.formatSimple
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
public data class MangaChapter(
/**
* An unique id of chapter
*/
@JvmField val id: Long,
@JvmField public val id: Long,
/**
* User-readable name of chapter
* User-readable name of chapter if provided by parser or null instead
* Do not pass manga title or chapter number here
*/
@JvmField val name: String,
@JvmField public val title: String?,
/**
* Chapter number starting from 1
* Chapter number starting from 1, 0 if unknown
*/
@JvmField val number: Int,
@JvmField public val number: Float,
/**
* Volume number starting from 1, 0 if unknown
*/
@JvmField public val volume: Int,
/**
* Relative url to chapter (**without** a domain) or any other uri.
* Used principally in parsers
*/
@JvmField val url: String,
@JvmField public val url: String,
/**
* User-readable name of scanlator (releaser) or null if unknown
*/
@JvmField val scanlator: String?,
@JvmField public val scanlator: String?,
/**
* Chapter upload date in milliseconds
*/
@JvmField val uploadDate: Long,
@JvmField public val uploadDate: Long,
/**
* User-readable name of branch.
* A branch is a group of chapters that overlap (e.g. different languages)
*/
@JvmField val branch: String?,
@JvmField val source: MangaSource,
) : Comparable<MangaChapter> {
@JvmField public val branch: String?,
@JvmField public val source: MangaSource,
) {
override fun compareTo(other: MangaChapter): Int {
return number.compareTo(other.number)
@Deprecated("Use title instead", ReplaceWith("title"))
val name: String
get() = title.ifNullOrEmpty {
buildString {
if (volume > 0) append("Vol ").append(volume).append(' ')
if (number > 0) append("Chapter ").append(number.formatSimple()) else append("Unnamed")
}
}
public fun numberString(): String? = if (number > 0f) {
number.formatSimple()
} else {
null
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaChapter
if (id != other.id) return false
if (name != other.name) return false
if (number != other.number) return false
if (url != other.url) return false
if (scanlator != other.scanlator) return false
if (uploadDate != other.uploadDate) return false
if (branch != other.branch) return false
if (source != other.source) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + number
result = 31 * result + url.hashCode()
result = 31 * result + (scanlator?.hashCode() ?: 0)
result = 31 * result + uploadDate.hashCode()
result = 31 * result + (branch?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaChapter($id - #$number [$url] - $source)"
public fun volumeString(): String? = if (volume > 0) {
volume.toString()
} else {
null
}
}

View File

@@ -2,28 +2,87 @@ package org.koitharu.kotatsu.parsers.model
import java.util.*
sealed interface MangaListFilter {
public data class MangaListFilter(
@JvmField val query: String? = null,
@JvmField val tags: Set<MangaTag> = emptySet(),
@JvmField val tagsExclude: Set<MangaTag> = emptySet(),
@JvmField val locale: Locale? = null,
@JvmField val originalLocale: Locale? = null,
@JvmField val states: Set<MangaState> = emptySet(),
@JvmField val contentRating: Set<ContentRating> = emptySet(),
@JvmField val types: Set<ContentType> = emptySet(),
@JvmField val demographics: Set<Demographic> = emptySet(),
@JvmField val year: Int = YEAR_UNKNOWN,
@JvmField val yearFrom: Int = YEAR_UNKNOWN,
@JvmField val yearTo: Int = YEAR_UNKNOWN,
@JvmField val author: String? = null,
) {
fun isEmpty(): Boolean
private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() &&
tagsExclude.isEmpty() &&
locale == null &&
originalLocale == null &&
states.isEmpty() &&
contentRating.isEmpty() &&
year == YEAR_UNKNOWN &&
yearFrom == YEAR_UNKNOWN &&
yearTo == YEAR_UNKNOWN &&
types.isEmpty() &&
demographics.isEmpty() &&
author.isNullOrEmpty()
val sortOrder: SortOrder?
public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty()
data class Search(
@JvmField val query: String,
) : MangaListFilter {
public fun isNotEmpty(): Boolean = !isEmpty()
override val sortOrder: SortOrder? = null
public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty()
override fun isEmpty() = query.isBlank()
public companion object {
@JvmStatic
public val EMPTY: MangaListFilter = MangaListFilter()
}
data class Advanced(
override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>,
@JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>,
) : MangaListFilter {
internal class Builder {
private var query: String? = null
private val tags: MutableSet<MangaTag> = mutableSetOf()
private val tagsExclude: MutableSet<MangaTag> = mutableSetOf()
private var locale: Locale? = null
private var originalLocale: Locale? = null
private val states: MutableSet<MangaState> = mutableSetOf()
private val contentRating: MutableSet<ContentRating> = mutableSetOf()
private val types: MutableSet<ContentType> = mutableSetOf()
private val demographics: MutableSet<Demographic> = mutableSetOf()
private var year: Int = YEAR_UNKNOWN
private var yearFrom: Int = YEAR_UNKNOWN
private var yearTo: Int = YEAR_UNKNOWN
override fun isEmpty(): Boolean = tags.isEmpty() && locale == null && states.isEmpty()
fun query(query: String?): Builder = apply { this.query = query }
fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) }
fun addTags(tags: Collection<MangaTag>): Builder = apply { this.tags.addAll(tags) }
fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) }
fun excludeTags(tags: Collection<MangaTag>): Builder = apply { this.tagsExclude.addAll(tags) }
fun locale(locale: Locale?): Builder = apply { this.locale = locale }
fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale }
fun addState(state: MangaState): Builder = apply { states.add(state) }
fun addStates(states: Collection<MangaState>): Builder = apply { this.states.addAll(states) }
fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) }
fun addContentRatings(ratings: Collection<ContentRating>): Builder =
apply { this.contentRating.addAll(ratings) }
fun addType(type: ContentType): Builder = apply { types.add(type) }
fun addTypes(types: Collection<ContentType>): Builder = apply { this.types.addAll(types) }
fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) }
fun addDemographics(demographics: Collection<Demographic>): Builder =
apply { this.demographics.addAll(demographics) }
fun year(year: Int): Builder = apply { this.year = year }
fun yearFrom(year: Int): Builder = apply { this.yearFrom = year }
fun yearTo(year: Int): Builder = apply { this.yearTo = year }
fun build(): MangaListFilter = MangaListFilter(
query, tags, tagsExclude, locale, originalLocale, states,
contentRating, types, demographics, year, yearFrom, yearTo,
)
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
public data class MangaListFilterCapabilities @InternalParsersApi constructor(
/**
* Whether parser supports filtering by more than one tag
* @see [MangaListFilter.tags]
* @see [MangaListFilterOptions.availableTags]
*/
val isMultipleTagsSupported: Boolean = false,
/**
* Whether parser supports tagsExclude field in filter
* @see [MangaListFilter.tagsExclude]
* @see [MangaListFilterOptions.availableTags]
*/
val isTagsExclusionSupported: Boolean = false,
/**
* Whether parser supports searching by string query
* @see [MangaListFilter.query]
*/
val isSearchSupported: Boolean = false,
/**
* Whether parser supports searching by string query combined within other filters
*/
val isSearchWithFiltersSupported: Boolean = false,
/**
* Whether parser supports searching/filtering by year
* @see [MangaListFilter.year]
*/
val isYearSupported: Boolean = false,
/**
* Whether parser supports searching by year range
* @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo]
*/
val isYearRangeSupported: Boolean = false,
/**
* Whether parser supports searching Original Languages
* @see [MangaListFilter.originalLocale]
* @see [MangaListFilterOptions.availableLocales]
*/
val isOriginalLocaleSupported: Boolean = false,
/**
* Whether parser supports searching by author name
* @see [MangaListFilter.author]
*/
val isAuthorSearchSupported: Boolean = false,
)

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
import java.util.*
public data class MangaListFilterOptions @InternalParsersApi constructor(
/**
* Available tags (genres)
*/
public val availableTags: Set<MangaTag> = emptySet(),
/**
* Supported [MangaState] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableStates: Set<MangaState> = emptySet(),
/**
* Supported [ContentRating] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableContentRating: Set<ContentRating> = emptySet(),
/**
* Supported [ContentType] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableContentTypes: Set<ContentType> = emptySet(),
/**
* Supported [Demographic] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
public val availableDemographics: Set<Demographic> = emptySet(),
/**
* Supported content locales for multilingual sources
*/
public val availableLocales: Set<Locale> = emptySet(),
)

View File

@@ -2,46 +2,21 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
class MangaPage(
public data class MangaPage(
/**
* Unique identifier for manga
* Unique identifier for page
*/
@JvmField val id: Long,
@JvmField public val id: Long,
/**
* Relative url to page (**without** a domain) or any other uri.
* Used principally in parsers.
* May contain link to image or html page.
* @see MangaParser.getPageUrl
*/
@JvmField val url: String,
@JvmField public val url: String,
/**
* Absolute url of the small page image if exists, null otherwise
*/
@JvmField val preview: String?,
@JvmField val source: MangaSource,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaPage
if (id != other.id) return false
if (url != other.url) return false
if (preview != other.preview) return false
return source == other.source
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + (preview?.hashCode() ?: 0)
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaPage($id [$url] - $source)"
}
}
@JvmField public val preview: String?,
@JvmField public val source: MangaSource,
)

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.parsers.model
public interface MangaSource {
public val name: String
}

View File

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

View File

@@ -2,40 +2,15 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
class MangaTag(
public data class MangaTag(
/**
* User-readable tag title, should be in Title case
*/
@JvmField val title: String,
@JvmField public val title: String,
/**
* Identifier of a tag, must be unique among the source.
* @see MangaParser.getList
*/
@JvmField val key: String,
@JvmField val source: MangaSource,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaTag
if (title != other.title) return false
if (key != other.key) return false
if (source != other.source) return false
return true
}
override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + key.hashCode()
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "MangaTag($key \"$title\" - $source)"
}
}
@JvmField public val key: String,
@JvmField public val source: MangaSource,
)

View File

@@ -1,9 +1,22 @@
package org.koitharu.kotatsu.parsers.model
enum class SortOrder {
public enum class SortOrder {
UPDATED,
UPDATED_ASC,
POPULARITY,
POPULARITY_ASC,
RATING,
RATING_ASC,
NEWEST,
ALPHABETICAL
}
NEWEST_ASC,
ALPHABETICAL,
ALPHABETICAL_DESC,
ADDED,
ADDED_ASC,
RELEVANCE,
POPULARITY_HOUR,
POPULARITY_TODAY,
POPULARITY_WEEK,
POPULARITY_MONTH,
POPULARITY_YEAR,
}

View File

@@ -3,9 +3,9 @@ package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.InternalParsersApi
@InternalParsersApi
class WordSet(private vararg val words: String) {
public class WordSet(private vararg val words: String) {
fun anyWordIn(dateString: String): Boolean = words.any {
dateString.contains(it, ignoreCase = true)
}
}
public fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
public fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) }
public fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) }
}

View File

@@ -0,0 +1,90 @@
package org.koitharu.kotatsu.parsers.model.search
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import org.koitharu.kotatsu.parsers.model.SortOrder
/**
* Represents a search query for filtering and sorting manga search results.
* This class is immutable and must be constructed using the [Builder].
*
* @property criteria The set of search criteria applied to the query.
* @property order The sorting order for the results (optional).
* @property offset The offset number for paginated search results (optional).
*/
@Deprecated("Too complex. Use MangaListFilter instead")
@ConsistentCopyVisibility
public data class MangaSearchQuery private constructor(
@JvmField public val criteria: Set<QueryCriteria<*>>,
@JvmField public val order: SortOrder?,
@JvmField public val offset: Int,
@JvmField public val skipValidation: Boolean,
) {
public fun newBuilder(): Builder = Builder(this)
public class Builder {
private val criteria = ArraySet<QueryCriteria<*>>()
private var order: SortOrder? = null
private var offset: Int = 0
private var skipValidation: Boolean = false
public constructor()
public constructor(query: MangaSearchQuery) : this() {
criteria.addAll(query.criteria)
order = query.order
offset = query.offset
}
public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) }
public fun order(order: SortOrder?): Builder = apply { this.order = order }
public fun offset(offset: Int): Builder = apply { this.offset = offset }
public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip }
@Throws(IllegalArgumentException::class)
public fun build(): MangaSearchQuery {
return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation)
}
private fun deduplicateCriteria(criteria: Set<QueryCriteria<*>>): Set<QueryCriteria<*>> {
val uniqueCriteria =
ArrayMap<Pair<SearchableField, Class<out QueryCriteria<*>>>, QueryCriteria<*>>(criteria.size)
for (criterion in criteria) {
val key = criterion.field to criterion::class.java
val existing = uniqueCriteria[key]
when {
existing == null -> uniqueCriteria[key] = criterion
existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> {
uniqueCriteria[key] =
QueryCriteria.Include(criterion.field, existing.values union criterion.values)
}
existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> {
uniqueCriteria[key] =
QueryCriteria.Exclude(criterion.field, existing.values union criterion.values)
}
else -> throw IllegalArgumentException(
"Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}",
)
}
}
return uniqueCriteria.values.toSet()
}
}
public companion object {
public val EMPTY: MangaSearchQuery = MangaSearchQuery(emptySet(), null, 0, false)
}
}

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.parsers.model.search
import androidx.collection.ArraySet
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
import org.koitharu.kotatsu.parsers.util.mapToSet
@Deprecated("Too complex. Use MangaListFilterCapabilities instead")
@ExposedCopyVisibility
public data class MangaSearchQueryCapabilities internal constructor(
public val capabilities: Set<SearchCapability>,
) {
public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities))
internal fun validate(query: MangaSearchQuery) {
val strictFields = capabilities.filter { it.isExclusive }.mapToSet { it.field }
val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields)
require(usedStrictFields.isEmpty() || query.criteria.size <= 1) {
"Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria."
}
for (criterion in query.criteria) {
val capability = requireNotNull(capabilities.find { it.field == criterion.field }) {
"Unsupported search field: ${criterion.field}"
}
require(criterion::class in capability.criteriaTypes) {
"Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}"
}
// Ensure single value per criterion if supportMultiValue is false
if (!capability.isMultiple) {
when (criterion) {
is Include<*> -> require(criterion.values.size <= 1) {
"Multiple values are not allowed for field ${criterion.field}"
}
is Exclude<*> -> require(criterion.values.size <= 1) {
"Multiple values are not allowed for field ${criterion.field}"
}
is Range<*> -> Unit // Range is always valid (from, to)
is Match<*> -> Unit // Match always has a single value
}
}
}
}
}

View File

@@ -0,0 +1,106 @@
package org.koitharu.kotatsu.parsers.model.search
/**
* Represents a generic search criterion used for filtering manga search results.
* Each criterion applies a specific condition to a [SearchableField] and operates on values of type [T].
*
* @param T The type of value associated with the search criterion.
* @property field The field to which this search criterion applies.
*/
@Deprecated("Too complex")
public sealed interface QueryCriteria<T> {
public val field: SearchableField
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
/**
* Represents an inclusion criterion that allows search results based on a set of allowed values.
*
* @param T The type of value being included in the search.
* @property values The set of values that should be included in the search results.
*
* ### Example Usage:
* ```kotlin
* val genreFilter = QueryCriteria.Include(SearchableField.STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))
* ```
*/
public data class Include<T : Any>(
public override val field: SearchableField,
@JvmField public val values: Set<T>,
) : QueryCriteria<T> {
init {
check(values.all { x -> field.type.isInstance(x) })
}
}
/**
* Represents an exclusion criterion that exclude results containing certain values.
*
* @param T The type of value being excluded from the search.
* @property values The set of values that should be excluded from the search results.
*
* ### Example Usage:
* ```kotlin
* val excludeTag = QueryCriteria.Exclude(SearchableField.TAG, setOf(MangaTag(key, title, source)))
* ```
*/
public data class Exclude<T : Any>(
public override val field: SearchableField,
@JvmField public val values: Set<T>,
) : QueryCriteria<T> {
init {
check(values.all { x -> field.type.isInstance(x) })
}
}
/**
* Represents a range criterion that allows search based on a range of values.
*
* @param T The type of value used in the range (must be comparable).
* @property from The starting value of the range (inclusive).
* @property to The ending value of the range (inclusive).
*
* ### Example Usage:
* ```kotlin
* val yearRange = QueryCriteria.Range(SearchableField.PUBLICATION_YEAR, 2000, 2020)
* ```
*/
public data class Range<T : Comparable<T>>(
public override val field: SearchableField,
@JvmField public val from: T,
@JvmField public val to: T,
) : QueryCriteria<T> {
init {
check(field.type.isInstance(from))
check(field.type.isInstance(to))
}
}
/**
* Represents a match criterion that search results based on an exact match of a value.
*
* @param T The type of value being matched.
* @property value The exact value that must be matched.
*
* ### Example Usage:
* ```kotlin
* val titleMatch = QueryCriteria.Match(SearchableField.TITLE, "manga title")
* ```
*/
public data class Match<T : Any>(
public override val field: SearchableField,
@JvmField public val value: T,
) : QueryCriteria<T> {
init {
check(field.type.isInstance(value))
}
}
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.parsers.model.search
import kotlin.reflect.KClass
/**
* Defines the search capabilities of a given field in the manga search query.
*
* @property field The searchable field that this capability applies to.
* Example values:
* - `SearchableField.TITLE_NAME` for searching by title.
* - `SearchableField.AUTHOR` for searching by author names.
* - `SearchableField.TAG` for filtering by tags.
* @property criteriaTypes The set of supported criteria types for the field.
* Example values:
* - `setOf(Include::class, Exclude::class)` selected field supports inclusion/exclusion criteria.
* - `setOf(Range::class)` selected field support numerical range criteria.
* @property isMultiValue Indicates whether the field supports multiple values.
* - `true` if multiple values can be provided (e.g., multiple tags or authors).
* - `false` if only a single value is allowed (e.g., only one tag or author).
* @property isExclusive Specifies whether the field can be used alongside other criteria.
* - `true` if this field can be used with other search criteria.
* - `false` if using this field requires it to be the only criterion in query.
*/
@Deprecated("Too complex")
public data class SearchCapability(
/** The searchable field that this capability applies to. */
@JvmField public val field: SearchableField,
/** The set of supported criteria types for this field. */
@JvmField public val criteriaTypes: Set<KClass<out QueryCriteria<*>>>,
/** Indicates whether the field supports multiple values. */
@JvmField public val isMultiple: Boolean,
/** Specifies whether the field can be used alongside other criteria. */
@JvmField public val isExclusive: Boolean = false,
)

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.parsers.model.search
import org.koitharu.kotatsu.parsers.model.*
import java.util.*
/**
* Represents the various fields that can be used for searching manga.
* Each field is associated with a specific data type that defines its expected values.
*
* @property type The Java class representing the expected type of values for this field.
*/
@Deprecated("Too complex")
public enum class SearchableField(public val type: Class<*>) {
TITLE_NAME(String::class.java),
TAG(MangaTag::class.java),
AUTHOR(MangaTag::class.java),
LANGUAGE(Locale::class.java),
ORIGINAL_LANGUAGE(Locale::class.java),
STATE(MangaState::class.java),
CONTENT_TYPE(ContentType::class.java),
CONTENT_RATING(ContentRating::class.java),
DEMOGRAPHIC(Demographic::class.java),
PUBLICATION_YEAR(Int::class.javaObjectType);
}

View File

@@ -0,0 +1,47 @@
package org.koitharu.kotatsu.parsers.network
import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import org.jsoup.Jsoup
import java.net.HttpURLConnection.HTTP_FORBIDDEN
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
public object CloudFlareHelper {
public const val PROTECTION_NOT_DETECTED: Int = 0
public const val PROTECTION_CAPTCHA: Int = 1
public const val PROTECTION_BLOCKED: Int = 2
private const val CF_CLEARANCE = "cf_clearance"
public fun checkResponseForProtection(response: Response): Int {
if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) {
return PROTECTION_NOT_DETECTED
}
val content = try {
response.peekBody(Long.MAX_VALUE).use {
Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString())
}
} catch (_: IllegalStateException) {
return PROTECTION_NOT_DETECTED
}
return when {
content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null -> PROTECTION_BLOCKED
content.getElementById("challenge-error-title") != null || content.getElementById("challenge-error-text") != null -> PROTECTION_CAPTCHA
else -> PROTECTION_NOT_DETECTED
}
}
public fun getClearanceCookie(cookieJar: CookieJar, url: String): String? {
return cookieJar.loadForRequest(url.toHttpUrl()).find { it.name == CF_CLEARANCE }?.value
}
public fun isCloudFlareCookie(name: String): Boolean {
return name.startsWith("cf_")
|| name.startsWith("_cf")
|| name.startsWith("__cf")
|| name == "csrftoken"
}
}

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson
import java.net.HttpURLConnection
class OkHttpWebClient(
public class OkHttpWebClient(
private val httpClient: OkHttpClient,
private val mangaSource: MangaSource,
) : WebClient {

View File

@@ -1,14 +1,17 @@
package org.koitharu.kotatsu.parsers.network
object UserAgents {
public object UserAgents {
const val CHROME_MOBILE =
public const val CHROME_MOBILE: String =
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 Mobile Safari/537.36"
const val CHROME_DESKTOP =
public const val FIREFOX_MOBILE: String =
"Mozilla/5.0 (Android 14; Mobile; LG-M255; rv:123.0) Gecko/123.0 Firefox/123.0"
public const val CHROME_DESKTOP: String =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
const val FIREFOX_DESKTOP = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
const val KOTATSU = "Kotatsu/5.3 (Android 13;;; en)"
public const val KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)"
}

View File

@@ -6,54 +6,55 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import org.json.JSONObject
interface WebClient {
public interface WebClient {
/**
* Do a GET http request to specific url
* @param url
*/
suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl())
public suspend fun httpGet(url: String): Response = httpGet(url.toHttpUrl())
suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders)
public suspend fun httpGet(url: String, extraHeaders: Headers?): Response = httpGet(url.toHttpUrl(), extraHeaders)
/**
* Do a GET http request to specific url
* @param url
*/
suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null)
public suspend fun httpGet(url: HttpUrl): Response = httpGet(url, null)
/**
* Do a GET http request to specific url
* @param url
* @param extraHeaders additional HTTP headers for request
*/
suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response
public suspend fun httpGet(url: HttpUrl, extraHeaders: Headers?): Response
/**
* Do a HEAD http request to specific url
* @param url
*/
suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl())
public suspend fun httpHead(url: String): Response = httpHead(url.toHttpUrl())
/**
* Do a HEAD http request to specific url
* @param url
*/
suspend fun httpHead(url: HttpUrl): Response
public suspend fun httpHead(url: HttpUrl): Response
/**
* Do a POST http request to specific url with `multipart/form-data` payload
* @param url
* @param form payload as key=>value map
*/
suspend fun httpPost(url: String, form: Map<String, String>): Response = httpPost(url.toHttpUrl(), form, null)
public suspend fun httpPost(url: String, form: Map<String, String>): Response =
httpPost(url.toHttpUrl(), form, null)
/**
* Do a POST http request to specific url with `multipart/form-data` payload
* @param url
* @param form payload as key=>value map
*/
suspend fun httpPost(url: HttpUrl, form: Map<String, String>): Response = httpPost(url, form, null)
public suspend fun httpPost(url: HttpUrl, form: Map<String, String>): Response = httpPost(url, form, null)
/**
* Do a POST http request to specific url with `multipart/form-data` payload
@@ -61,21 +62,21 @@ interface WebClient {
* @param form payload as key=>value map
* @param extraHeaders additional HTTP headers for request
*/
suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response
public suspend fun httpPost(url: HttpUrl, form: Map<String, String>, extraHeaders: Headers?): Response
/**
* Do a POST http request to specific url with `multipart/form-data` payload
* @param url
* @param payload payload as `key=value` string with `&` separator
*/
suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null)
public suspend fun httpPost(url: String, payload: String): Response = httpPost(url.toHttpUrl(), payload, null)
/**
* Do a POST http request to specific url with `multipart/form-data` payload
* @param url
* @param payload payload as `key=value` string with `&` separator
*/
suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null)
public suspend fun httpPost(url: HttpUrl, payload: String): Response = httpPost(url, payload, null)
/**
* Do a POST http request to specific url with `multipart/form-data` payload
@@ -83,21 +84,21 @@ interface WebClient {
* @param payload payload as `key=value` string with `&` separator
* @param extraHeaders additional HTTP headers for request
*/
suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response
public suspend fun httpPost(url: HttpUrl, payload: String, extraHeaders: Headers?): Response
/**
* Do a POST http request to specific url with json payload
* @param url
* @param body
*/
suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null)
public suspend fun httpPost(url: String, body: JSONObject): Response = httpPost(url.toHttpUrl(), body, null)
/**
* Do a POST http request to specific url with json payload
* @param url
* @param body
*/
suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null)
public suspend fun httpPost(url: HttpUrl, body: JSONObject): Response = httpPost(url, body, null)
/**
* Do a POST http request to specific url with json payload
@@ -105,12 +106,12 @@ interface WebClient {
* @param body
* @param extraHeaders additional HTTP headers for request
*/
suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response
public suspend fun httpPost(url: HttpUrl, body: JSONObject, extraHeaders: Headers?): Response
/**
* Do a GraphQL request to specific url
* @param endpoint an url
* @param query GraphQL request payload
*/
suspend fun graphQLQuery(endpoint: String, query: String): JSONObject
public suspend fun graphQLQuery(endpoint: String, query: String): JSONObject
}

View File

@@ -5,9 +5,10 @@ import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@@ -21,16 +22,76 @@ import javax.crypto.spec.SecretKeySpec
@MangaSourceParser("BATOTO", "Bato.To")
internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
context = context,
source = MangaSource.BATOTO,
source = MangaParserSource.BATOTO,
pageSize = 60,
searchPageSize = 20,
) {
), MangaParserAuthProvider {
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val authUrl: String
get() = "https://${domain}/signin"
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name.contains("skey")
}
}
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${domain}/account/profiles").parseHtml().body()
return body.selectFirst("ul.toggleMenu-content:has(.avatar):has(a) div.text-center div")?.text()
?: body.parseFailed("Cannot find username")
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.POPULARITY,
SortOrder.POPULARITY_YEAR,
SortOrder.POPULARITY_MONTH,
SortOrder.POPULARITY_WEEK,
SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_HOUR,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isOriginalLocaleSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
MangaState.PAUSED,
MangaState.UPCOMING,
),
availableContentRating = EnumSet.of(ContentRating.SAFE),
availableLocales = 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"),
),
)
override val configKeyDomain = ConfigKey.Domain(
@@ -59,39 +120,97 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
"zbato.com",
"zbato.net",
"zbato.org",
"fto.to",
"jto.to",
)
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, order: SortOrder, filter: MangaListFilter): List<Manga> {
when {
!filter.query.isNullOrEmpty() -> {
return search(page, filter.query)
}
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
SortOrder.RATING -> Unit
else -> {
val url = buildString {
append("https://")
append(domain)
append("/browse?sort=")
when (order) {
SortOrder.UPDATED -> append("update.za")
SortOrder.POPULARITY -> append("views_a.za")
SortOrder.NEWEST -> append("create.za")
SortOrder.ALPHABETICAL -> append("title.az")
SortOrder.POPULARITY_YEAR -> append("views_y.za")
SortOrder.POPULARITY_MONTH -> append("views_m.za")
SortOrder.POPULARITY_WEEK -> append("views_w.za")
SortOrder.POPULARITY_TODAY -> append("views_d.za")
SortOrder.POPULARITY_HOUR -> append("views_h.za")
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"
else -> throw IllegalArgumentException("$it not supported")
},
)
}
filter.locale?.let {
append("&langs=")
if (it.language == "in") {
append("id")
} else {
append(it.language)
}
}
filter.originalLocale?.let {
append("&origs=")
if (it.language == "in") {
append("id")
} else {
append(it.language)
}
}
append("&genres=")
if (filter.tags.isNotEmpty()) {
filter.tags.joinTo(this, ",") { it.key }
}
append("|")
if (filter.tagsExclude.isNotEmpty()) {
filter.tagsExclude.joinTo(this, ",") { 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)
}
if (!tags.isNullOrEmpty()) {
append("&genres=")
appendAll(tags, ",") { it.key }
}
append("&page=")
append(page)
}
return parseList(url, page)
}
override suspend fun getDetails(manga: Manga): Manga {
@@ -99,22 +218,29 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
.requireElementById("mainer")
val details = root.selectFirstOrThrow(".detail-set")
val attrs = details.selectFirst(".attr-main")?.select(".attr-item")?.associate {
it.child(0).text().trim() to it.child(1)
it.child(0).text() to it.child(1)
}.orEmpty()
val author = attrs["Authors:"]?.textOrNull()
return manga.copy(
title = root.selectFirst("h3.item-title")?.text() ?: manga.title,
isNsfw = !root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty(),
contentRating = if (root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty()) {
ContentRating.ADULT
} else {
ContentRating.SAFE
},
largeCoverUrl = details.selectFirst("img[src]")?.absUrl("src"),
description = details.getElementById("limit-height-body-summary")
?.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,
authors = author?.let { setOf(it) } ?: manga.authors,
chapters = root.selectFirst(".episode-list")
?.selectFirst(".main")
?.children()
@@ -129,7 +255,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 +275,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,
)
@@ -159,7 +285,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
throw ParseException("Cannot find images list", fullUrl)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val scripts = webClient.httpGet(
"https://${domain}/browse",
).parseHtml().selectOrThrow("script")
@@ -170,7 +296,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,
)
@@ -214,17 +340,17 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
Manga(
id = generateUid(href),
title = title,
altTitle = div.selectFirst(".item-alias")?.text()?.takeUnless { it == title },
altTitles = setOfNotNull(div.selectFirst(".item-alias")?.textOrNull()?.takeUnless { it == title }),
url = href,
publicUrl = a.absUrl("href"),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirst("img[src]")?.absUrl("src").orEmpty(),
contentRating = null,
coverUrl = div.selectFirst("img[src]")?.absUrl("src"),
largeCoverUrl = null,
description = null,
tags = div.selectFirst(".item-genre")?.parseTags().orEmpty(),
state = null,
author = null,
authors = emptySet(),
source = source,
)
}
@@ -245,8 +371,9 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
val href = a.attrAsRelativeUrl("href")
return MangaChapter(
id = generateUid(href),
name = a.text(),
number = index + 1,
title = a.textOrNull(),
number = index + 1f,
volume = 0,
url = href,
scanlator = extra?.getElementsByAttributeValueContaining("href", "/group/")?.text(),
uploadDate = runCatching {

View File

@@ -2,98 +2,173 @@ package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat
import okhttp3.HttpUrl
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.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.text.SimpleDateFormat
import java.util.*
/**
* https://api.comick.fun/docs/static/index.html
*/
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, MangaParserSource.COMICK_FUN, 20) {
override val configKeyDomain = ConfigKey.Domain("comick.app")
override val configKeyDomain = ConfigKey.Domain("comick.io")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.UPDATED,
SortOrder.RATING,
SortOrder.NEWEST,
)
@Volatile
private var cachedTags: SparseArrayCompat<MangaTag>? = null
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
isYearRangeSupported = true,
)
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentTypes = EnumSet.of(
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
ContentType.OTHER,
),
availableDemographics = EnumSet.of(
Demographic.SHOUNEN,
Demographic.SHOUJO,
Demographic.SEINEN,
Demographic.JOSEI,
Demographic.NONE,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, 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()
}
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) {
SortOrder.POPULARITY -> "view"
SortOrder.RATING -> "rating"
else -> "uploaded"
},
)
}
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())
filter.query?.let {
url.addQueryParameter("q", filter.query)
}
val ja = webClient.httpGet(url).parseJsonArray()
val tagsMap = cachedTags ?: loadTags()
filter.tags.forEach {
url.addQueryParameter("genres", it.key)
}
filter.tagsExclude.forEach {
url.addQueryParameter("excludes", it.key)
}
url.addQueryParameter(
"sort",
when (order) {
SortOrder.NEWEST -> "created_at"
SortOrder.POPULARITY -> "view"
SortOrder.RATING -> "rating"
SortOrder.UPDATED -> "uploaded"
else -> "uploaded"
},
)
filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter(
"status",
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.ABANDONED -> "3"
MangaState.PAUSED -> "4"
else -> ""
},
)
}
if (filter.yearFrom != YEAR_UNKNOWN) {
url.addQueryParameter("from", filter.yearFrom.toString())
}
if (filter.yearTo != YEAR_UNKNOWN) {
url.addQueryParameter("to", filter.yearTo.toString())
}
filter.types.forEach {
url.addQueryParameter(
"country",
when (it) {
ContentType.MANGA -> "jp"
ContentType.MANHWA -> "kr"
ContentType.MANHUA -> "cn"
ContentType.OTHER -> "others"
else -> ""
},
)
}
filter.demographics.forEach {
url.addQueryParameter(
"demographic",
when (it) {
Demographic.SHOUNEN -> "1"
Demographic.SHOUJO -> "2"
Demographic.SEINEN -> "3"
Demographic.JOSEI -> "4"
Demographic.NONE -> "5"
else -> ""
},
)
}
val ja = webClient.httpGet(url.build()).parseJsonArray()
val tagsMap = tagsArray.get()
return ja.mapJSON { jo ->
val slug = jo.getString("slug")
Manga(
id = generateUid(slug),
title = jo.getString("title"),
altTitle = null,
altTitles = emptySet(),
url = slug,
publicUrl = "https://$domain/comic/$slug",
rating = jo.getDoubleOrDefault("rating", -10.0).toFloat() / 10f,
isNsfw = false,
coverUrl = jo.getString("cover_url"),
contentRating = null,
coverUrl = jo.getStringOrNull("cover_url"),
largeCoverUrl = null,
description = jo.getStringOrNull("desc"),
tags = jo.selectGenres(tagsMap),
state = runCatching {
if (jo.getBoolean("translation_completed")) {
MangaState.FINISHED
} else {
MangaState.ONGOING
}
}.getOrNull(),
author = null,
state = when (jo.getIntOrDefault("status", 0)) {
1 -> MangaState.ONGOING
2 -> MangaState.FINISHED
3 -> MangaState.ABANDONED
4 -> MangaState.PAUSED
else -> null
},
authors = emptySet(),
source = source,
)
}
@@ -104,11 +179,18 @@ 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")
val alt = comic.getJSONArray("md_titles").asTypedList<JSONObject>().mapNotNullToSet {
it.getStringOrNull("title")
}
val authors = jo.getJSONArray("artists").mapJSONNotNullToSet { it.getStringOrNull("name") }
return manga.copy(
title = comic.getString("title"),
altTitle = null, // TODO
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
description = comic.getStringOrNull("parsed") ?: comic.getString("desc"),
altTitles = alt,
contentRating = when {
comic.getBooleanOrDefault("hentai", false) -> ContentRating.ADULT
jo.getBooleanOrDefault("matureContent", false) -> ContentRating.SUGGESTIVE
else -> ContentRating.SAFE
},
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
val g = it.getJSONObject("md_genres")
MangaTag(
@@ -117,11 +199,44 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
source = source,
)
},
author = jo.getJSONArray("artists").optJSONObject(0)?.getString("name"),
authors = authors,
chapters = getChapters(comic.getString("hid")),
)
}
private suspend fun getChapters(hid: String): List<MangaChapter> {
val ja = webClient.httpGet(
url = "https://api.${domain}/comic/$hid/chapters?limit=$CHAPTERS_LIMIT",
).parseJson().getJSONArray("chapters")
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
return ja.asTypedList<JSONObject>().reversed().mapChapters { _, jo ->
val vol = jo.getIntOrDefault("vol", 0)
val chap = jo.getFloatOrDefault("chap", 0f)
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")),
title = jo.getStringOrNull("title"),
number = chap,
volume = vol,
url = jo.getString("hid"),
scanlator = jo.optJSONArray("group_name")?.asTypedList<String>()?.joinToString()
?.takeUnless { it.isBlank() },
uploadDate = dateFormat.parseSafe(jo.getString("created_at").substringBefore('T')),
branch = branch,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val jo = webClient.httpGet(
"https://api.${domain}/chapter/${chapter.url}?tachiyomi=true",
@@ -137,8 +252,15 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val sparseArray = cachedTags ?: loadTags()
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? {
val slug = link.pathSegments.lastOrNull() ?: return null
return resolver.resolveManga(this, url = slug, id = generateUid(slug))
}
private val tagsArray = suspendLazy(initializer = ::loadTags)
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val sparseArray = tagsArray.get()
val set = ArraySet<MangaTag>(sparseArray.size())
for (i in 0 until sparseArray.size()) {
set.add(sparseArray.valueAt(i))
@@ -149,69 +271,31 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
private suspend fun loadTags(): SparseArrayCompat<MangaTag> {
val ja = webClient.httpGet("https://api.${domain}/genre").parseJsonArray()
val tags = SparseArrayCompat<MangaTag>(ja.length())
for (jo in ja.JSONIterator()) {
for (jo in ja.asTypedList<JSONObject>()) {
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
}
private suspend fun getChapters(hid: String): List<MangaChapter> {
val ja = webClient.httpGet(
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 chaptersBuilder = ChaptersListBuilder(list.size)
val branchedChapters = HashMap<String?, HashMap<Pair<String?, String?>, MangaChapter>>()
for (jo in list) {
val vol = jo.getStringOrNull("vol")
val chap = jo.getStringOrNull("chap")
val volChap = vol to chap
val locale = Locale.forLanguageTag(jo.getString("lang"))
val lc = locale.getDisplayName(locale).toTitleCase(locale)
val branch = (list.indices).firstNotNullOf { i ->
val b = if (i == 0) lc else "$lc ($i)"
if (branchedChapters[b]?.get(volChap) == null) b else null
}
val chapter = 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 = branchedChapters[branch]?.size?.plus(1) ?: 1,
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,
)
if (chaptersBuilder.add(chapter)) {
branchedChapters.getOrPut(branch, ::HashMap)[volChap] = chapter
}
}
return chaptersBuilder.toList()
}
private fun JSONObject.selectGenres(tags: SparseArrayCompat<MangaTag>): Set<MangaTag> {
val array = optJSONArray("genres") ?: return emptySet()
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,310 +1,497 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.SparseArrayCompat
import androidx.collection.set
import androidx.collection.ArraySet
import androidx.collection.MutableIntLongMap
import androidx.collection.MutableIntObjectMap
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.bitmap.Rect
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
import java.util.Collections.emptyList
import java.util.concurrent.TimeUnit
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
private const val DOMAIN_AUTHORIZED = "exhentai.org"
private val TAG_PREFIXES = arrayOf("male:", "female:", "other:")
private const val BANNED_RESPONSE_LENGTH = 256L
@MangaSourceParser("EXHENTAI", "ExHentai", type = ContentType.HENTAI)
internal class ExHentaiParser(
context: MangaLoaderContext,
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
context: MangaLoaderContext,
) : PagedMangaParser(context, MangaParserSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider, Interceptor {
override val availableSortOrders: Set<SortOrder> = Collections.singleton(
SortOrder.NEWEST,
)
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain(
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
)
override val configKeyDomain: ConfigKey.Domain
get() {
val isAuthorized = checkAuth()
return ConfigKey.Domain(
if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED,
if (isAuthorized) DOMAIN_UNAUTHORIZED else DOMAIN_AUTHORIZED,
)
}
override val authUrl: String
get() = "https://${domain}/bounce_login.php"
override val authUrl: String
get() = "https://${domain}/bounce_login.php"
private val ratingPattern = Regex("-?[0-9]+px")
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 ratingPattern = Regex("-?[0-9]+px")
private val titleCleanupPattern = Regex("(\\[.*?]|\\([C0-9]*\\))")
private val spacesCleanupPattern = Regex("(^\\s+|\\s+\$|\\s+(?=\\s))")
private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash")
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val nextPages = MutableIntObjectMap<MutableIntLongMap>()
override val isAuthorized: Boolean
get() {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
if (authorized) {
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
context.cookieJar.copyCookies(
DOMAIN_UNAUTHORIZED,
DOMAIN_AUTHORIZED,
authCookies,
)
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
}
return true
}
return false
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
isAuthorSearchSupported = true,
)
init {
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
context.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2")
paginator.firstPage = 0
}
override suspend fun isAuthorized(): Boolean = checkAuth()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
var search = query?.urlEncoded().orEmpty()
val next = nextPages.get(page, 0L)
if (page > 0 && next == 0L) {
assert(false) { "Page timestamp not found" }
return emptyList()
}
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 + " "
}
}
if (fCats != 0) {
append("&f_cats=")
append(1023 - fCats)
}
}
if (search.isNotEmpty()) {
append("&f_search=")
append(search.trim().replace(' ', '+'))
}
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
if (updateDm) {
append("&inline_set=dm_e")
}
append("&advsearch=1")
if (config[suspiciousContentKey]) {
append("&f_sh=on")
}
}
val body = webClient.httpGet(url).parseHtml().body()
val root = body.selectFirst("table.itg")
?.selectFirst("tbody")
?: if (updateDm) {
body.parseFailed("Cannot find root")
} else {
updateDm = true
return getListPage(page, query, tags, sortOrder)
}
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 href = a.attrAsRelativeUrl("href")
val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
val mainTag = td2.selectFirst("div.cn")?.let { div ->
MangaTag(
title = div.text().toTitleCase(),
key = tagIdByClass(div.classNames()) ?: return@let null,
source = source,
)
}
Manga(
id = generateUid(href),
title = glink.text().cleanupTitle(),
altTitle = null,
url = href,
publicUrl = a.absUrl("href"),
rating = td2.selectFirst("div.ir")?.parseRating() ?: RATING_UNKNOWN,
isNsfw = true,
coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(),
tags = setOfNotNull(mainTag),
state = null,
author = tagsDiv.getElementsContainingOwnText("artist:").first()
?.nextElementSibling()?.text(),
source = source,
)
}
}
init {
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
context.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2")
paginator.firstPage = 0
searchPaginator.firstPage = 0
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
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 tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
return manga.copy(
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle,
publicUrl = doc.baseUri().ifEmpty { manga.publicUrl },
rating = root.getElementById("rating_label")?.text()
?.substringAfterLast(' ')
?.toFloatOrNull()
?.div(5f) ?: manga.rating,
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
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"
},
chapters = tabs?.select("a")?.findLast { a ->
a.text().toIntOrNull() != null
}?.let { a ->
val count = a.text().toInt()
val chapters = ChaptersListBuilder(count)
for (i in 1..count) {
val url = "${manga.url}?p=${i - 1}"
chapters += MangaChapter(
id = generateUid(url),
name = "${manga.title} #$i",
number = i,
url = url,
uploadDate = 0L,
source = source,
scanlator = null,
branch = null,
)
}
chapters.toList()
},
)
}
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = mapTags(),
availableContentTypes = EnumSet.of(
ContentType.DOUJINSHI,
ContentType.MANGA,
ContentType.ARTIST_CG,
ContentType.GAME_CG,
ContentType.COMICS,
ContentType.IMAGE_SET,
ContentType.OTHER,
),
availableLocales = 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"),
),
)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body().requireElementById("gdt")
return root.select("a").map { a ->
val url = a.attrAsRelativeUrl("href")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return getListPage(page, order, filter, updateDm = false)
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
}
private suspend fun getListPage(
page: Int,
order: SortOrder,
filter: MangaListFilter,
updateDm: Boolean,
): List<Manga> {
val next = synchronized(nextPages) {
nextPages[filter.hashCode()]?.getOrDefault(page, 0L) ?: 0L
}
override suspend fun getAvailableTags(): Set<MangaTag> {
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(),
key = id.toString(),
source = source,
)
}
}
if (page > 0 && next == 0L) {
assert(false) { "Page timestamp not found" }
return emptyList()
}
override suspend fun getUsername(): String {
val doc = webClient.httpGet("https://forums.$DOMAIN_UNAUTHORIZED/").parseHtml().body()
val username = doc.getElementById("userlinks")
?.getElementsByAttributeValueContaining("href", "showuser=")
?.firstOrNull()
?.ownText()
?: if (doc.getElementById("userlinksguest") != null) {
throw AuthRequiredException(source)
} else {
doc.parseFailed()
}
return username
}
val url = urlBuilder()
url.addEncodedQueryParameter("next", next.toString())
url.addQueryParameter("f_search", filter.toSearchQuery())
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(suspiciousContentKey)
}
val fCats = filter.types.toFCats()
if (fCats != 0) {
url.addEncodedQueryParameter("f_cats", (1023 - fCats).toString())
}
if (updateDm) {
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
url.addQueryParameter("inline_set", "dm_e")
}
url.addQueryParameter("advsearch", "1")
if (config[suspiciousContentKey]) {
url.addQueryParameter("f_sh", "on")
}
val body = webClient.httpGet(url.build()).parseHtml().body()
val root = body.selectFirst("table.itg")?.selectFirst("tbody")
if (root == null) {
if (updateDm) {
if (body.getElementsContainingText("No hits found").isNotEmpty()) {
return emptyList()
} else {
body.parseFailed("Cannot find root")
}
} else {
return getListPage(page, order, filter, updateDm = true)
}
}
val nextTimestamp = getNextTimestamp(body)
synchronized(nextPages) {
nextPages.getOrPut(filter.hashCode()) {
MutableIntLongMap()
}.put(page + 1, nextTimestamp)
}
private fun isAuthorized(domain: String): Boolean {
val cookies = context.cookieJar.getCookies(domain).mapToSet { x -> x.name }
return authCookies.all { it in cookies }
}
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 href = a.attrAsRelativeUrl("href")
val tagsDiv = gLink.nextElementSibling() ?: gLink.parseFailed("tags div not found")
val rawTitle = gLink.text()
val author = tagsDiv.getElementsContainingOwnText("artist:").first()
?.nextElementSibling()?.textOrNull()
Manga(
id = generateUid(href),
title = rawTitle.cleanupTitle(),
altTitles = emptySet(),
url = href,
publicUrl = a.absUrl("href"),
rating = td2.selectFirst("div.ir")?.parseRating() ?: RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = td1.selectFirst("img")?.attrAsAbsoluteUrlOrNull("src"),
tags = tagsDiv.parseTags(),
state = when {
rawTitle.contains("(ongoing)", ignoreCase = true) -> MangaState.ONGOING
else -> null
},
authors = setOfNotNull(author),
source = source,
)
}
}
private fun Element.parseRating(): Float {
return runCatching {
val style = requireNotNull(attr("style"))
val (v1, v2) = ratingPattern.find(style)!!.destructured
var p1 = v1.dropLast(2).toInt()
val p2 = v2.dropLast(2).toInt()
if (p2 != -1) {
p1 += 8
}
(80 - p1) / 80f
}.getOrDefault(RATING_UNKNOWN)
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
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 tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
val gd3 = root.getElementById("gd3")
val lang = gd3
?.selectFirst("tr:contains(Language)")
?.selectFirst(".gdt2")?.ownTextOrNull()
val uploadDate = gd3
?.selectFirst("tr:contains(Posted)")
?.selectFirst(".gdt2")?.ownTextOrNull()
.let { SimpleDateFormat("yyyy-MM-dd HH:mm", sourceLocale).parseSafe(it) }
val uploader = gd3
?.getElementsByAttributeValueContaining("href", "/uploader/")
?.firstOrNull()
?.ownTextOrNull()
val tags = tagList?.parseTags().orEmpty()
private fun String.cleanupTitle(): String {
val result = StringBuilder(length)
var skip = false
for (c in this) {
when {
c == '[' -> skip = true
c == ']' -> skip = false
c.isWhitespace() && result.isEmpty() -> continue
!skip -> result.append(c)
}
}
while (result.lastOrNull()?.isWhitespace() == true) {
result.deleteCharAt(result.lastIndex)
}
return result.toString()
}
return manga.copy(
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
altTitles = setOfNotNull(title?.getElementById("gj")?.text()?.cleanupTitle()?.nullIfEmpty()),
publicUrl = doc.baseUri().ifEmpty { manga.publicUrl },
rating = root.getElementById("rating_label")?.text()
?.substringAfterLast(' ')
?.toFloatOrNull()
?.div(5f) ?: manga.rating,
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
tags = manga.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"
},
chapters = tabs?.select("a")?.findLast { a ->
a.text().toIntOrNull() != null
}?.let { a ->
val count = a.text().toInt()
val chapters = ChaptersListBuilder(count)
for (i in 1..count) {
val url = "${manga.url}?p=${i - 1}"
chapters += MangaChapter(
id = generateUid(url),
title = null,
number = i.toFloat(),
volume = 0,
url = url,
uploadDate = uploadDate,
source = source,
scanlator = uploader,
branch = lang,
)
}
chapters.toList()
},
)
}
private fun String.cssUrl(): String? {
val fromIndex = indexOf("url(")
if (fromIndex == -1) {
return null
}
val toIndex = indexOf(')', startIndex = fromIndex)
return if (toIndex == -1) {
null
} else {
substring(fromIndex + 4, toIndex).trim()
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body().requireElementById("gdt")
return root.select("a").map { a ->
val url = a.attrAsRelativeUrl("href")
MangaPage(
id = generateUid(url),
url = url,
preview = a.children().firstOrNull()?.extractPreview(),
source = source,
)
}
}
private fun tagIdByClass(classNames: Collection<String>): String? {
val className = classNames.find { x -> x.startsWith("ct") } ?: return null
val num = className.drop(2).toIntOrNull(16) ?: return null
return 2.0.pow(num).toInt().toString()
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
}
private fun getNextTimestamp(root: Element): Long {
return root.getElementById("unext")
?.attrAsAbsoluteUrlOrNull("href")
?.toHttpUrlOrNull()
?.queryParameter("next")
?.toLongOrNull() ?: 1
}
@Suppress("SpellCheckingInspection")
private val tags: String
get() = "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"
private fun mapTags(): Set<MangaTag> {
val tagElements = tags.split(",")
val result = ArraySet<MangaTag>(tagElements.size)
for (tag in tagElements) {
val el = tag.trim()
if (el.isEmpty()) continue
result += MangaTag(
title = el.toTitleCase(Locale.ENGLISH),
key = el,
source = source,
)
}
return result
}
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headersContentLength(BANNED_RESPONSE_LENGTH) <= BANNED_RESPONSE_LENGTH) {
val text = response.peekBody(BANNED_RESPONSE_LENGTH).use { it.string() }
if (text.contains("IP address has been temporarily banned", ignoreCase = true)) {
val hours = Regex("([0-9]+) hours?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0
val minutes = Regex("([0-9]+) minutes?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0
val seconds = Regex("([0-9]+) seconds?").find(text)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0
response.closeQuietly()
throw TooManyRequestExceptions(
url = response.request.url.toString(),
retryAfter = TimeUnit.HOURS.toMillis(hours)
+ TimeUnit.MINUTES.toMillis(minutes)
+ TimeUnit.SECONDS.toMillis(seconds),
)
}
}
val imageRect = response.request.url.fragment?.split(',')
if (imageRect != null && imageRect.size == 4) {
// rect: top,left,right,bottom
return context.redrawImageResponse(response) { bitmap ->
val srcRect = Rect(
left = imageRect[0].toInt(),
top = imageRect[1].toInt(),
right = imageRect[2].toInt(),
bottom = imageRect[3].toInt(),
)
val dstRect = Rect(0, 0, srcRect.width, srcRect.height)
val result = context.createBitmap(dstRect.width, dstRect.height)
result.drawBitmap(bitmap, srcRect, dstRect)
result
}
}
return response
}
private fun Locale.toLanguagePath() = when (language) {
else -> getDisplayLanguage(Locale.ENGLISH).lowercase()
}
override suspend fun getUsername(): String {
val doc = webClient.httpGet("https://forums.$DOMAIN_UNAUTHORIZED/").parseHtml().body()
val username = doc.getElementById("userlinks")
?.getElementsByAttributeValueContaining("href", "showuser=")
?.firstOrNull()
?.ownText()
?: if (doc.getElementById("userlinksguest") != null) {
throw AuthRequiredException(source)
} else {
doc.parseFailed()
}
return username
}
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
keys.add(suspiciousContentKey)
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val query = seed.title
return getListPage(
page = 0,
order = defaultSortOrder,
filter = MangaListFilter(query = query),
)
}
private fun isAuthorized(domain: String): Boolean {
val cookies = context.cookieJar.getCookies(domain).mapToSet { x -> x.name }
return authCookies.all { it in cookies }
}
private fun Element.parseRating(): Float {
return runCatching {
val style = requireNotNull(attr("style"))
val (v1, v2) = ratingPattern.findAll(style).toList()
var p1 = v1.groupValues.first().dropLast(2).toInt()
val p2 = v2.groupValues.first().dropLast(2).toInt()
if (p2 != -1) {
p1 += 8
}
(80 - p1) / 80f
}.getOrDefault(RATING_UNKNOWN)
}
private fun String.cleanupTitle(): String {
return replace(titleCleanupPattern, "")
.replace(spacesCleanupPattern, "")
}
private fun Element.parseTags(): Set<MangaTag> {
fun Element.parseTag() = textOrNull()?.let {
MangaTag(title = it.toTitleCase(Locale.ENGLISH), key = it, source = source)
}
val result = ArraySet<MangaTag>()
for (prefix in TAG_PREFIXES) {
getElementsByAttributeValueStarting("id", "ta_$prefix").mapNotNullTo(result, Element::parseTag)
getElementsByAttributeValueStarting("title", prefix).mapNotNullTo(result, Element::parseTag)
}
return result
}
private fun Element.extractPreview(): String? {
val bg = backgroundOrNull() ?: return null
return buildString {
append(bg.url)
append('#')
// rect: left,top,right,bottom
append(bg.left)
append(',')
append(bg.top)
append(',')
append(bg.right)
append(',')
append(bg.bottom)
}
}
private fun getNextTimestamp(root: Element): Long {
return root.getElementById("unext")
?.attrAsAbsoluteUrlOrNull("href")
?.toHttpUrlOrNull()
?.queryParameter("next")
?.toLongOrNull() ?: 1
}
private fun MangaListFilter.toSearchQuery(): String? {
if (isEmpty()) {
return null
}
val joiner = StringUtil.StringJoiner(" ")
if (!query.isNullOrEmpty()) {
joiner.add(query)
}
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("\"$")
}
if (!author.isNullOrEmpty()) {
joiner.add("artist:\"")
joiner.append(author)
joiner.append("\"$")
}
return joiner.complete().nullIfEmpty()
}
private fun Collection<ContentType>.toFCats(): Int = fold(0) { acc, ct ->
val cat: Int = when (ct) {
ContentType.DOUJINSHI -> 2
ContentType.MANGA -> 4
ContentType.ARTIST_CG -> 8
ContentType.GAME_CG -> 16
ContentType.COMICS -> 512
ContentType.IMAGE_SET -> 32
else -> 449 // 1 or 64 or 128 or 256
}
acc or cat
}
private fun checkAuth(): Boolean {
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
if (authorized) {
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
context.cookieJar.copyCookies(
DOMAIN_UNAUTHORIZED,
DOMAIN_AUTHORIZED,
authCookies,
)
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
}
return true
}
return false
}
}

View File

@@ -0,0 +1,773 @@
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.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
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.suspendlazy.suspendLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.LinkedList
import java.util.Locale
import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class)
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
internal class HitomiLaParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.HITOMILA) {
override val configKeyDomain = ConfigKey.Domain("hitomi.la")
private val cdnDomain = "gold-usergeneratedcontent.net"
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
private val ltnBaseUrl get() = "https://ltn.$cdnDomain"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_WEEK,
SortOrder.POPULARITY_MONTH,
SortOrder.POPULARITY_YEAR,
)
private val localeMap: Map<Locale, String> = mapOf(
Locale.forLanguageTag("id") to "indonesian",
Locale.forLanguageTag("jv") to "javanese",
Locale.forLanguageTag("ca") to "catalan",
Locale.forLanguageTag("ceb") to "cebuano",
Locale.forLanguageTag("cs") to "czech",
Locale.forLanguageTag("da") to "danish",
Locale.forLanguageTag("de") to "german",
Locale.forLanguageTag("et") to "estonian",
Locale.ENGLISH to "english",
Locale.forLanguageTag("es") to "spanish",
Locale.forLanguageTag("eo") to "esperanto",
Locale.forLanguageTag("fr") to "french",
Locale.forLanguageTag("it") to "italian",
Locale.forLanguageTag("hi") to "hindi",
Locale.forLanguageTag("hu") to "hungarian",
Locale.forLanguageTag("pl") to "polish",
Locale.forLanguageTag("pt") to "portuguese",
Locale.forLanguageTag("vi") to "vietnamese",
Locale.forLanguageTag("tr") to "turkish",
Locale.forLanguageTag("ru") to "russian",
Locale.forLanguageTag("uk") to "ukrainian",
Locale.forLanguageTag("ar") to "arabic",
Locale.KOREAN to "korean",
Locale.CHINESE to "chinese",
Locale.JAPANESE to "japanese",
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = localeMap.keys,
)
private fun Locale?.getSiteLang(): String = when (this) {
null -> "all"
else -> localeMap[this] ?: "all"
}
private suspend fun fetchAvailableTags(): 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, order: SortOrder, filter: MangaListFilter): List<Manga> = when {
filter.query.isNullOrEmpty() -> {
if (filter.tags.isEmpty()) {
when (order) {
SortOrder.POPULARITY_TODAY -> {
getGalleryIDsFromNozomi(
"popular",
"today",
filter.locale.getSiteLang(),
offset.nextOffsetRange(),
)
}
SortOrder.POPULARITY_WEEK -> {
getGalleryIDsFromNozomi(
"popular",
"week",
filter.locale.getSiteLang(),
offset.nextOffsetRange(),
)
}
SortOrder.POPULARITY_MONTH -> {
getGalleryIDsFromNozomi(
"popular",
"month",
filter.locale.getSiteLang(),
offset.nextOffsetRange(),
)
}
SortOrder.POPULARITY_YEAR -> {
getGalleryIDsFromNozomi(
"popular",
"year",
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 },
order,
filter.locale.getSiteLang(),
).toList()
}
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
}
}
else -> {
if (offset == 0) {
cachedSearchIds = hitomiSearch(filter.query, order).toList()
}
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
}
}.toMangaList()
private fun Int.nextOffsetRange(): LongRange {
val bytes = this * 4L
return bytes.until(bytes + 100L)
}
private suspend fun hitomiSearch(
query: String,
sortByPopularity: SortOrder = SortOrder.UPDATED,
language: String = "all",
): Set<Int> =
coroutineScope {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.splitByWhitespace()
.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 == SortOrder.UPDATED -> getGalleryIDsFromNozomi(null, "index", language)
sortByPopularity == SortOrder.POPULARITY_TODAY -> getGalleryIDsFromNozomi("popular", "today", language)
sortByPopularity == SortOrder.POPULARITY_WEEK -> getGalleryIDsFromNozomi("popular", "week", language)
sortByPopularity == SortOrder.POPULARITY_MONTH -> getGalleryIDsFromNozomi("popular", "month", language)
sortByPopularity == SortOrder.POPULARITY_YEAR -> getGalleryIDsFromNozomi("popular", "year", 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
check(keySize in 1..32) { "Invalid key size $keySize" }
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 > img")
.attr("data-src"),
publicUrl =
doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href")
.toAbsoluteUrl(domain),
authors = emptySet(),
tags = emptySet(),
contentRating = ContentRating.ADULT,
rating = RATING_UNKNOWN,
altTitles = emptySet(),
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)
val author =
json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString()
return manga.copy(
title = json.getString("title"),
largeCoverUrl =
json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash")
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${subDomain}tn.$cdnDomain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
},
authors = setOfNotNull(author),
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,
title = json.getStringOrNull("title"),
scanlator = json.getString("type").toTitleCase(),
number = 1f,
volume = 0,
branch = json.getString("language_localname"),
source = source,
uploadDate = dateFormat.parseSafe(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 = subdomainOffset(imageId) + 1
val thumbSubdomain = 'a' + subdomainOffset(imageId)
MangaPage(
id = generateUid(hash),
url = "https://a${subDomain}.$cdnDomain/$commonId$imageId/$hash.avif",
preview = "https://${thumbSubdomain}tn.$cdnDomain/webpsmallsmalltn/${thumbPathFromHash(hash)}/$hash.webp",
source = source,
)
}
}
// / --->
private var scriptLastRetrieval: Long = -1L
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 == -1L || (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")
}
// rewrite_tn_paths <-- common.js
private suspend fun rewriteTnPaths(html: String): String {
val thumbUrlRegex = Regex(
"""(?<protocol>//)(?<host>[a-z0-9.-]+\.(?:hitomi\.la|${Regex.escape(cdnDomain)}))/(?<pathAfterHost>(?:avif|webp)?(?:small)?(?:big|small|medium)tn/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}\.(?:webp|avif|gif|png|jpe?g))""",
)
var resultHtml = html
thumbUrlRegex.findAll(html).forEach { matchResult ->
val originalUrl = matchResult.value
val groups = matchResult.groups
val pathAfterHost = groups["pathAfterHost"]?.value ?: return@forEach
val newTnSubdomain = subdomainFromURL(originalUrl, "tn")
val correctedUrl = "${groups["protocol"]!!.value}$newTnSubdomain.$cdnDomain/$pathAfterHost"
if (originalUrl != correctedUrl) {
resultHtml = resultHtml.replace(originalUrl, correctedUrl)
}
}
return resultHtml
}
private suspend fun subdomainFromURL(url: String, base: String?): String {
val resultSubdomain = base ?: "b"
// This regex extracts the last 3 hex characters from the hash in the URL
// The hash is 64 characters, so we look for the 61st character onward
val hashRegex = Regex("""/([0-9a-f]{61}[0-9a-f]{3})[./]""")
val fullHashMatch = hashRegex.find(url)
?: // If no hash is found, default to "a" + base (typically "atn")
return "a$resultSubdomain"
val fullHash = fullHashMatch.groupValues[1]
val lastThreeChars = fullHash.takeLast(3)
val lastDigit = lastThreeChars.last()
val lastTwoDigits = lastThreeChars.take(2)
val imageId = "$lastDigit$lastTwoDigits".toIntOrNull(16)
return if (imageId != null) {
('a' + subdomainOffset(imageId)).toString() + resultSubdomain
} else {
"a$resultSubdomain"
}
}
private fun String.toTagTitle(): String {
return toCamelCase()
.replace("♂", "(male)")
.replace("♀", "(female)")
}
}

View File

@@ -0,0 +1,140 @@
package org.koitharu.kotatsu.parsers.site.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.parseSafe
import java.text.SimpleDateFormat
import java.util.Locale
@MangaSourceParser("HOLOEARTH", "HoloEarth")
internal class HoloEarthParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.HOLOEARTH, 3) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("holoearth.com")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = false,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableLocales = setOf(
Locale("en"),
Locale.JAPANESE,
Locale("id"),
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://$domain")
filter.locale?.let {
append(
when (it) {
Locale("en") -> "/en"
Locale.JAPANESE -> ""
Locale("id") -> "/id"
else -> "" // default
}
)
}
append("/alt/holonometria/manga")
}
val doc = webClient.httpGet(url).parseHtml()
val root = doc.body().selectFirstOrThrow(".manga__list")
val mangaList = root.select("li .manga__item-inner")
if (mangaList.isEmpty()) return emptyList()
return mangaList.mapNotNull { li ->
val coverUrl = li.getElementsByTag("img").attr("src")
val title = li.getElementsByClass("manga__title").text()
val altTitle = li.getElementsByClass("manga__copy").text()
val description = li.getElementsByClass("manga__caption").text()
val url = li.getElementsByTag("a").attr("href")
Manga(
id = generateUid(url),
title = title,
altTitles = setOf(altTitle),
url = url,
publicUrl = url,
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = coverUrl,
tags = emptySet(),
state = null,
authors = emptySet(),
source = source,
description = description,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url).parseHtml()
val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.US)
val root = doc.body().selectFirstOrThrow(".manga-detail__wrapper")
val coverUrl = root.selectFirstOrThrow(".manga-detail__thumb img").attr("src")
val chapters = root.select(".manga-detail__list-item")
val mangaChapters = chapters.mapIndexed { index, li ->
val url = li.selectFirstOrThrow(".manga-detail__list-link").attr("href")
val title = li.selectFirstOrThrow(".manga-detail__list-title").text()
val dateStr = li.selectFirstOrThrow(".manga-detail__list-date").text()
val uploadDate = dateFormat.parseSafe(dateStr) ?: 0L
val scanlator = root.selectFirst(".manga-detail__person")?.text()
MangaChapter(
id = generateUid(url),
title = title,
number = index + 1f,
volume = 0,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = null,
source = source,
)
}
return manga.copy(
coverUrl = coverUrl,
chapters = mangaChapters,
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url).parseHtml()
val imageList = doc.body().selectFirstOrThrow(".manga-detail__swiper-wrapper")
val images = imageList.select(".manga-detail__swiper-slide").reversed()
return images.mapNotNull { page ->
val img = page.selectFirst("img") ?: return@mapNotNull null
val src = img.attr("src")
MangaPage(
id = generateUid(src),
url = src,
preview = src,
source = source,
)
}
}
}

View File

@@ -7,60 +7,110 @@ import kotlinx.coroutines.coroutineScope
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.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("IMHENTAI", "ImHentai", type = ContentType.HENTAI)
internal class ImHentai(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.IMHENTAI, pageSize = 20) {
PagedMangaParser(context, MangaParserSource.IMHENTAI, pageSize = 20) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING)
override val configKeyDomain = ConfigKey.Domain("imhentai.xxx")
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = setOf(
Locale.ENGLISH,
Locale.JAPANESE,
Locale("es"),
Locale.FRENCH,
Locale("kr"),
Locale.GERMAN,
Locale("ru"),
),
availableContentTypes = EnumSet.of(
ContentType.MANGA,
ContentType.DOUJINSHI,
ContentType.COMICS,
ContentType.IMAGE_SET,
ContentType.ARTIST_CG,
ContentType.GAME_CG,
),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
order: SortOrder,
filter: MangaListFilter,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
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.query.isNullOrEmpty() -> {
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")
else -> {
if (filter.tags.isNotEmpty()) {
append("&key=")
filter.tags.joinTo(this, separator = ",") { it.key }
}
var types = "&m=1&d=1&w=1&i=1&a=1&g=1"
if (filter.types.isNotEmpty()) {
types = "&m=0&d=0&w=0&i=0&a=0&g=0"
filter.types.forEach {
when (it) {
ContentType.MANGA -> types = types.replace("&m=0", "&m=1")
ContentType.DOUJINSHI -> types = types.replace("&d=0", "&d=1")
ContentType.COMICS -> types = types.replace("&w=0", "&w=1")
ContentType.IMAGE_SET -> types = types.replace("&i=0", "&i=1")
ContentType.ARTIST_CG -> types = types.replace("&a=0", "&a=1")
ContentType.GAME_CG -> types = types.replace("&g=0", "&g=1")
else -> {}
}
}
}
append(types)
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 (order) {
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")
}
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.galleries div.thumb").map { div ->
@@ -70,30 +120,30 @@ internal class ImHentai(context: MangaLoaderContext) :
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = a.selectFirst("img")?.src().orEmpty(),
coverUrl = a.selectFirst("img")?.src(),
title = div.selectFirst(".caption")?.text().orEmpty(),
altTitle = null,
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
authors = emptySet(),
state = null,
source = source,
isNsfw = isNsfwSource,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
)
}
}
//Tags are deliberately reduced because there are too many and this slows down the application.
//only the most popular ones are taken.
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return coroutineScope {
(1..3).map { page ->
async { getTags(page) }
async { fetchTagsPage(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
private suspend fun fetchTagsPage(page: Int): Set<MangaTag> {
val url = "https://$domain/tags/popular/?page=$page"
val root = webClient.httpGet(url).parseHtml()
return root.parseTags()
@@ -103,7 +153,7 @@ 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,
)
}
@@ -111,26 +161,27 @@ internal class ImHentai(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val author = doc.selectFirst("li:contains(Artists) a.tag")?.ownTextOrNull()
manga.copy(
tags = doc.body().select("li:contains(Tags) a.tag").mapNotNullToSet {
val href = it.attr("href").substringAfterLast("tag/").substringBeforeLast('/')
val name = it.html().substringBeforeLast("<span")
MangaTag(
key = href,
title = name,
title = it.ownText().toTitleCase(sourceLocale),
source = source,
)
},
author = doc.selectFirst("li:contains(Artists) a.tag")?.html()?.substringBefore("<span"),
authors = setOfNotNull(author),
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
title = null,
number = 1f,
volume = 0,
url = manga.url,
scanlator = null,
uploadDate = 0,
branch = doc.selectFirst("li:contains(Language) a.tag")?.html()?.substringBeforeLast("<span"),
branch = doc.selectFirst("li:contains(Language) a.tag")?.ownTextOrNull()?.toTitleCase(sourceLocale),
source = source,
),
),
@@ -147,15 +198,15 @@ internal class ImHentai(context: MangaLoaderContext) :
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = a.selectFirst("img")?.src().orEmpty(),
coverUrl = a.selectFirst("img")?.src(),
title = div.selectFirst(".caption")?.text().orEmpty(),
altTitle = null,
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
authors = emptySet(),
state = null,
source = source,
isNsfw = false,
contentRating = null,
)
}
}
@@ -163,20 +214,28 @@ internal class ImHentai(context: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val totalPages = doc.selectFirstOrThrow(".pages").text().replace("Pages: ", "").toInt() + 1
val domainImg = doc.requireElementById("append_thumbs").selectFirstOrThrow("img").src()?.replace("1t.jpg", "")
val totalPages = doc.selectFirstOrThrow(".pages").text().replace("Pages: ", "").toInt()
val baseImg = doc.requireElementById("append_thumbs").selectFirstOrThrow("img")
val baseUrl = baseImg.selectFirstParentOrThrow("a").attrAsRelativeUrl("href").replace("/1/", "/\$/")
val baseThumbUrl = baseImg.src()?.replace("/1t.", "/\$t.")
val pages = ArrayList<MangaPage>(totalPages)
for (i in 1 until totalPages) {
val url = "$domainImg$i.jpg"
repeat(totalPages) { i ->
val url = baseUrl.replace("\$", (i + 1).toString())
pages.add(
MangaPage(
id = generateUid(url),
url = url,
preview = null,
preview = baseThumbUrl?.replace("\$", (i + 1).toString()),
source = source,
),
)
}
return pages
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val img = doc.body().requireElementById("gimg")
return img.requireSrc()
}
}

View File

@@ -0,0 +1,421 @@
package org.koitharu.kotatsu.parsers.site.all
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.json.JSONObject
import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
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.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNullToSet
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrDefault
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need to fix getPages, most manga don't have chapter images due to faulty fetch logic")
@MangaSourceParser("KOHARU", "Schale.network", type = ContentType.HENTAI)
internal class Koharu(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.KOHARU, 24) {
override val configKeyDomain = ConfigKey.Domain("niyaniya.moe")
private val apiSuffix = "api.schale.network"
override val userAgentKey = ConfigKey.UserAgent(
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.46 Mobile Safari/537.36",
)
private val authorsIds = suspendLazy { fetchAuthorsIds() }
private val preferredImageResolutionKey = ConfigKey.PreferredImageServer(
presetValues = mapOf(
"0" to "Lowest Quality",
"780" to "Low Quality (780px)",
"980" to "Medium Quality (980px)",
"1280" to "High Quality (1280px)",
"1600" to "Highest Quality (1600px)",
),
defaultValue = "1280",
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
keys.add(preferredImageResolutionKey)
}
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.add("referer", "https://$domain/")
.add("origin", "https://$domain")
.build()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.POPULARITY,
SortOrder.POPULARITY_TODAY,
SortOrder.POPULARITY_WEEK,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.RATING,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
isAuthorSearchSupported = true,
isSearchWithFiltersSupported = true,
isTagsExclusionSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchTags(namespace = 0),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val baseUrl = "https://$apiSuffix/books"
val url = buildString {
append(baseUrl)
val terms: MutableList<String> = mutableListOf()
val includedTags: MutableList<String> = mutableListOf()
val excludedTags: MutableList<String> = mutableListOf()
if (!filter.query.isNullOrEmpty() && filter.query.startsWith("id:")) {
val ipk = filter.query.removePrefix("id:")
val response = webClient.httpGet("$baseUrl/detail/$ipk").parseJson()
return listOf(parseMangaDetail(response))
}
val sortValue = when (order) {
SortOrder.POPULARITY, SortOrder.POPULARITY_TODAY -> "8"
SortOrder.POPULARITY_WEEK -> "9"
SortOrder.ALPHABETICAL -> "2"
SortOrder.ALPHABETICAL_DESC -> "2"
SortOrder.RATING -> "3"
SortOrder.NEWEST -> "4"
else -> "4"
}
append("?sort=").append(sortValue)
if (!filter.query.isNullOrEmpty()) {
terms.add("title:\"${filter.query.urlEncoded()}\"")
}
if (!filter.author.isNullOrEmpty()) {
val authors = authorsIds.getOrDefault(emptyMap())
val authorId = authors[filter.author.lowercase()]
if (authorId != null) {
includedTags.add(authorId)
} else {
terms.add("artist:\"${filter.author.urlEncoded()}\"")
}
}
filter.tags.forEach { tag ->
if (tag.key.startsWith("-")) {
excludedTags.add(tag.key.substring(1))
} else {
includedTags.add(tag.key)
}
}
if (excludedTags.isNotEmpty()) {
append("&exclude=").append(excludedTags.joinToString(","))
append("&e=1")
}
if (includedTags.isNotEmpty()) {
append("&include=").append(includedTags.joinToString(","))
append("&i=1")
}
append("&page=").append(page)
if (terms.isNotEmpty()) {
append("&s=").append(terms.joinToString(" ").urlEncoded())
}
}
val json = webClient.httpGet(url).parseJson()
json.getStringOrNull("error")?.let {
throw ParseException(it, url)
}
json.getStringOrNull("message")?.let {
throw ParseException(it, url)
}
return parseMangaList(json)
}
private fun parseMangaList(json: JSONObject): List<Manga> {
val entries = json.optJSONArray("entries") ?: return emptyList()
val results = ArrayList<Manga>(entries.length())
for (i in 0 until entries.length()) {
val entry = entries.getJSONObject(i)
val id = entry.getLong("id")
val key = entry.getString("key")
val url = "$id/$key"
results.add(
Manga(
id = generateUid(id),
url = url,
publicUrl = "https://$domain/g/$url",
title = entry.getString("title"),
altTitles = emptySet(),
authors = emptySet(),
tags = emptySet(),
rating = RATING_UNKNOWN,
state = null,
coverUrl = entry.getJSONObject("thumbnail").getString("path"),
contentRating = ContentRating.ADULT,
source = source,
),
)
}
return results
}
private fun parseMangaDetail(json: JSONObject): Manga {
val data = json.getJSONObject("data")
val id = data.getLong("id")
val key = data.getString("key")
val url = "$id/$key"
var author: String? = null
val tags = data.optJSONArray("tags")
if (tags != null) {
for (i in 0 until tags.length()) {
val tag = tags.getJSONObject(i)
if (tag.getInt("namespace") == 1) {
author = tag.getString("name")
break
}
}
}
return Manga(
id = generateUid(id),
url = url,
publicUrl = "https://$domain/g/$url",
title = data.getString("title"),
altTitles = emptySet(),
authors = setOfNotNull(author),
tags = emptySet(),
rating = RATING_UNKNOWN,
state = null,
coverUrl = data.getJSONObject("thumbnails").getJSONObject("main").getString("path"),
contentRating = ContentRating.ADULT,
source = source,
)
}
override suspend fun getDetails(manga: Manga): Manga {
val url = manga.url
val response = webClient.httpGet("https://$apiSuffix/books/detail/$url").parseJson()
val id = response.getLong("id")
val key = response.getString("key")
val mangaUrl = "$id/$key"
val tagsList = mutableSetOf<MangaTag>()
var author: String? = null
val tags = response.optJSONArray("tags")
if (tags != null) {
for (i in 0 until tags.length()) {
val tag = tags.getJSONObject(i)
if (tag.has("namespace")) {
val namespace = tag.getInt("namespace")
val tagName = tag.getString("name")
when (namespace) {
1 -> {
author = tagName
}
0, 3, 8, 9, 10, 12 -> {
tagsList.add(
MangaTag(
key = tagName,
title = tagName.toTitleCase(sourceLocale),
source = source,
),
)
}
}
} else {
val tagName = tag.getString("name")
tagsList.add(
MangaTag(
key = tagName,
title = tagName.toTitleCase(sourceLocale),
source = source,
),
)
}
}
}
val description = buildString {
val created = response.getLongOrDefault("created_at", 0L)
if (created > 0) {
append("<b>Posted:</b> ").append(SimpleDateFormat("yyyy-MM-dd", Locale.US).format(created)).append("\n")
}
val thumbnails = response.getJSONObject("thumbnails")
val pageCount = thumbnails.optJSONArray("entries")?.length() ?: 0
append("<b>Pages:</b> ").append(pageCount)
}
val thumbnails = response.getJSONObject("thumbnails")
val base = thumbnails.getString("base")
val mainPath = thumbnails.getJSONObject("main").getString("path")
val coverUrl = base + mainPath
return Manga(
id = generateUid(id),
url = mangaUrl,
publicUrl = "https://$domain/g/$mangaUrl",
title = response.getString("title"),
altTitles = emptySet(),
authors = setOfNotNull(author),
tags = tagsList,
rating = RATING_UNKNOWN,
state = MangaState.FINISHED,
description = description,
coverUrl = coverUrl,
contentRating = ContentRating.ADULT,
source = source,
chapters = listOf(
MangaChapter(
id = generateUid("$mangaUrl/chapter"),
title = null,
number = 1f,
url = mangaUrl,
scanlator = null,
uploadDate = response.getLongOrDefault("created_at", 0L),
branch = null,
source = source,
volume = 0,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val mangaUrl = chapter.url
val parts = mangaUrl.split('/')
if (parts.size < 2) {
throw ParseException("Invalid URL", mangaUrl)
}
val id = parts[0]
val key = parts[1]
val clearance = getClearance(chapter.publicUrl())
val dataUrl = "https://$apiSuffix/books/detail/$id/$key?crt=$clearance"
val data = try {
webClient.httpPost(
url = dataUrl.toHttpUrl(),
form = emptyMap(),
extraHeaders = getRequestHeaders(),
).parseJson().getJSONObject("data")
} catch (e: HttpStatusException) {
if (e.statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
// Token may be invalid or expired
// WebView should be closed after receiving Token
context.requestBrowserAction(this, chapter.publicUrl())
}
throw e
}
val preferredRes = config[preferredImageResolutionKey] ?: "1280"
val resolutionOrder = when (preferredRes) {
"1600" -> listOf("1600", "1280", "0", "980", "780")
"1280" -> listOf("1280", "1600", "0", "980", "780")
"980" -> listOf("980", "1280", "0", "1600", "780")
"780" -> listOf("780", "980", "0", "1280", "1600")
else -> listOf("0", "1600", "1280", "980", "780")
}
var selectedImageId: Int? = null
var selectedPublicKey: String? = null
var selectedQuality = "0"
for (res in resolutionOrder) {
if (data.has(res) && !data.isNull(res)) {
val resData = data.getJSONObject(res)
if (resData.has("id") && resData.has("key")) {
selectedImageId = resData.getInt("id")
selectedPublicKey = resData.getString("key")
selectedQuality = res
break
}
}
}
if (selectedImageId == null || selectedPublicKey == null) {
throw ParseException("Cant find image data", dataUrl)
}
val imagesResponse = webClient.httpGet(
"https://$apiSuffix/books/data/$id/$key/$selectedImageId/$selectedPublicKey/$selectedQuality?crt=$clearance",
).parseJson()
val base = imagesResponse.getString("base")
val entries = imagesResponse.getJSONArray("entries")
val pages = ArrayList<MangaPage>(entries.length())
for (i in 0 until entries.length()) {
val imagePath = entries.getJSONObject(i).getString("path")
val fullImageUrl = "$base$imagePath"
pages.add(
MangaPage(
id = generateUid(fullImageUrl),
url = fullImageUrl,
preview = null,
source = source,
),
)
}
return pages
}
private suspend fun fetchTags(namespace: Int): Set<MangaTag> =
webClient.httpGet("https://$apiSuffix/books/tags/filters").parseJsonArray().mapJSONNotNullToSet {
if (it.getIntOrDefault("namespace", 0) != namespace) {
null
} else {
MangaTag(
title = it.getStringOrNull("name")
?.toTitleCase(sourceLocale) ?: return@mapJSONNotNullToSet null,
key = it.getStringOrNull("id") ?: return@mapJSONNotNullToSet null,
source = source,
)
}
}
private suspend fun fetchAuthorsIds(): Map<String, String> = fetchTags(namespace = 1)
.associate { it.title.lowercase() to it.key }
private suspend fun getClearance(chapterUrl: String): String = WebViewHelper(context)
.getLocalStorageValue(domain, "clearance")?.removeSurrounding('"')?.nullIfEmpty()
?: context.requestBrowserAction(this, chapterUrl)
private fun MangaChapter.publicUrl() = "https://$domain/g/$url/read/1"
}

View File

@@ -2,14 +2,13 @@ package org.koitharu.kotatsu.parsers.site.all
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
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.core.AbstractMangaParser
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.*
@@ -21,10 +20,17 @@ import javax.crypto.spec.SecretKeySpec
internal abstract class LineWebtoonsParser(
context: MangaLoaderContext,
source: MangaSource,
) : MangaParser(context, source) {
source: MangaParserSource,
) : AbstractMangaParser(context, source) {
override val isMultipleTagsSupported = false
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
)
private val signer by lazy {
WebtoonsUrlSigner("gUtPzJFZch4ZyAGviiyH94P99lQ3pFdRTwpJWDlSGFfwgpr6ses5ALOxWHOIT7R1")
@@ -48,10 +54,13 @@ internal abstract class LineWebtoonsParser(
SortOrder.RATING,
SortOrder.UPDATED,
)
override val headers: Headers
get() = Headers.Builder()
.add("User-Agent", "nApps (Android 12;; linewebtoon; 3.1.0)")
.build()
override val userAgentKey = ConfigKey.UserAgent("nApps (Android 12;; linewebtoon; 3.1.0)")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getPageUrl(page: MangaPage): String {
return page.url.toAbsoluteUrl(staticDomain)
@@ -77,7 +86,7 @@ internal abstract class LineWebtoonsParser(
val episodes = firstResult
.getJSONObject("episodeList")
.getJSONArray("episode")
.toJSONList()
.asTypedList<JSONObject>()
.toMutableList()
while (episodes.count() < totalEpisodeCount) {
@@ -85,7 +94,7 @@ internal abstract class LineWebtoonsParser(
url = "/lineWebtoon/webtoon/challengeEpisodeList.json?v=2&titleNo=$titleNo&startIndex=${episodes.count()}&pageSize=30",
).getJSONObject("episodeList")
.getJSONArray("episode")
.toJSONList()
.asTypedList<JSONObject>()
episodes.addAll(page)
}
@@ -93,8 +102,9 @@ internal abstract class LineWebtoonsParser(
return episodes.mapChapters { i, jo ->
MangaChapter(
id = generateUid("$titleNo-$i"),
name = jo.getString("episodeTitle"),
number = jo.getInt("episodeSeq"),
title = jo.getStringOrNull("episodeTitle"),
number = jo.getInt("episodeSeq").toFloat(),
volume = 0,
url = "$titleNo-${jo.get("episodeNo")}",
uploadDate = jo.getLong("modifyYmdt"),
branch = null,
@@ -111,18 +121,20 @@ internal abstract class LineWebtoonsParser(
makeRequest("/lineWebtoon/webtoon/challengeTitleInfo.json?v=2&titleNo=${titleNo}")
.getJSONObject("titleInfo")
.let { jo ->
val isNsfwSource = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource)
val author = jo.getStringOrNull("writingAuthorName")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitle = null,
altTitles = emptySet(),
url = "$titleNo",
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=${titleNo}",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOf(parseTag(jo.getJSONObject("genreInfo"))),
author = jo.getStringOrNull("writingAuthorName"),
authors = setOfNotNull(author),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
@@ -132,84 +144,84 @@ 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, order: SortOrder, filter: MangaListFilter): List<Manga> {
val manga = when {
!filter.query.isNullOrEmpty() -> {
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 author = jo.getStringOrNull("writingAuthorName")
val sortOrderStr = when (sortOrder) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
}
val manga = if (query != null) {
if (!tags.isNullOrEmpty()) {
throw IllegalArgumentException("This source does not support search with tags")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitles = emptySet(),
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = RATING_UNKNOWN,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = emptySet(),
authors = setOfNotNull(author),
description = null,
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")
else -> {
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 genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
val sortOrderStr = when (order) {
SortOrder.UPDATED -> "UPDATE"
SortOrder.POPULARITY -> "READ_COUNT"
SortOrder.RATING -> "LIKEIT"
else -> throw IllegalArgumentException("Unsupported sort order: $order")
}
} 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 }
val result =
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
val genres = result.getJSONObject("genreList")
.getJSONArray("challengeGenres")
.mapJSON { jo -> parseTag(jo) }
.associateBy { tag -> tag.key }
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,
)
}
result
.getJSONObject("titleList")
.getJSONArray("titles")
.mapJSON { jo ->
val titleNo = jo.getLong("titleNo")
val isNsfwSource = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource)
val author = jo.getStringOrNull("writingAuthorName")
Manga(
id = generateUid(titleNo),
title = jo.getString("title"),
altTitles = emptySet(),
url = titleNo.toString(),
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
tags = setOfNotNull(genres[jo.getString("representGenre")]),
authors = setOfNotNull(author),
description = jo.getString("synopsis"),
// I don't think the API provides this info
state = null,
source = source,
)
}
}
}
return manga
}
@@ -229,6 +241,11 @@ internal abstract class LineWebtoonsParser(
}
}
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? {
val titleNo = link.queryParameter("title_no") ?: return null
return resolver.resolveManga(this, url = titleNo)
}
private fun parseTag(jo: JSONObject): MangaTag {
return MangaTag(
title = jo.getString("name"),
@@ -237,7 +254,7 @@ internal abstract class LineWebtoonsParser(
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
return makeRequest("/lineWebtoon/webtoon/challengeGenreList.json")
.getJSONObject("genreList")
.getJSONArray("challengeGenres")
@@ -273,25 +290,25 @@ internal abstract class LineWebtoonsParser(
}
@MangaSourceParser("LINEWEBTOONS_EN", "LineWebtoons English", "en", type = ContentType.MANGA)
class English(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_EN)
class English(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_EN)
@MangaSourceParser("LINEWEBTOONS_ZH", "LineWebtoons Chinese", "zh", type = ContentType.MANGA)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ZH)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ZH)
@MangaSourceParser("LINEWEBTOONS_TH", "LineWebtoons Thai", "th", type = ContentType.MANGA)
class Thai(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_TH)
class Thai(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_TH)
@MangaSourceParser("LINEWEBTOONS_ID", "LineWebtoons Indonesian", "id", type = ContentType.MANGA)
class Indonesian(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ID)
class Indonesian(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ID)
@MangaSourceParser("LINEWEBTOONS_ES", "LineWebtoons Spanish", "es", type = ContentType.MANGA)
class Spanish(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ES)
class Spanish(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ES)
@MangaSourceParser("LINEWEBTOONS_FR", "LineWebtoons French", "fr", type = ContentType.MANGA)
class French(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_FR)
class French(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_FR)
@MangaSourceParser("LINEWEBTOONS_DE", "LineWebtoons German", "de", type = ContentType.MANGA)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_DE)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_DE)
private inner class WebtoonsUrlSigner(private val secret: String) {
@@ -309,10 +326,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

@@ -4,13 +4,21 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl
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.core.FlexibleMangaParser
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
import org.koitharu.kotatsu.parsers.model.search.SearchCapability
import org.koitharu.kotatsu.parsers.model.search.SearchableField
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat
@@ -21,133 +29,265 @@ 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"
private const val SERVER_DATA = "data"
private const val SERVER_DATA_SAVER = "data-saver"
@MangaSourceParser("MANGADEX", "MangaDex")
internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGADEX) {
internal class MangaDexParser(context: MangaLoaderContext) : FlexibleMangaParser(context, MangaParserSource.MANGADEX) {
override val configKeyDomain = ConfigKey.Domain("mangadex.org")
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.POPULARITY,
private val preferredServerKey = ConfigKey.PreferredImageServer(
presetValues = mapOf(
SERVER_DATA to "Original quality",
SERVER_DATA_SAVER to "Compressed quality",
),
defaultValue = SERVER_DATA,
)
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val domain = domain
val url = buildString {
append("https://api.")
append(domain)
append("/manga?limit=")
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"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.POPULARITY -> "[followedCount]=desc"
else -> "[followedCount]=desc"
},
)
}
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
return json.mapJSON { jo ->
val id = jo.getString("id")
val attrs = jo.getJSONObject("attributes")
val relations = jo.getJSONArray("relationships").associateByKey("type")
val cover = relations["cover_art"]
?.getJSONObject("attributes")
?.getString("fileName")
?.let {
"https://uploads.$domain/covers/$id/$it"
}
Manga(
id = generateUid(id),
title = requireNotNull(attrs.getJSONObject("title").selectByLocale()) {
"Title should not be null"
},
altTitle = attrs.optJSONObject("altTitles")?.selectByLocale(),
url = id,
publicUrl = "https://$domain/title/$id",
rating = RATING_UNKNOWN,
isNsfw = attrs.getStringOrNull("contentRating") == "erotica",
coverUrl = cover?.plus(".256.jpg").orEmpty(),
largeCoverUrl = cover,
description = attrs.optJSONObject("description")?.selectByLocale(),
tags = attrs.getJSONArray("tags").mapJSONToSet { tag ->
MangaTag(
title = tag.getJSONObject("attributes")
.getJSONObject("name")
.firstStringValue()
.toTitleCase(),
key = tag.getString("id"),
source = source,
)
},
state = when (jo.getStringOrNull("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
else -> null
},
author = (relations["author"] ?: relations["artist"])
?.getJSONObject("attributes")
?.getStringOrNull("name"),
source = source,
)
}
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
keys.add(preferredServerKey)
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val domain = domain
val mangaId = manga.url.removePrefix("/")
val attrsDeferred = async {
webClient.httpGet(
"https://api.$domain/manga/${mangaId}?includes[]=artist&includes[]=author&includes[]=cover_art",
).parseJson().getJSONObject("data").getJSONObject("attributes")
}
val feedDeferred = async { loadChapters(mangaId) }
val mangaAttrs = attrsDeferred.await()
val feed = feedDeferred.await()
manga.copy(
description = mangaAttrs.optJSONObject("description")?.selectByLocale()
?: manga.description,
chapters = mapChapters(feed),
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.UPDATED_ASC,
SortOrder.POPULARITY,
SortOrder.POPULARITY_ASC,
SortOrder.RATING,
SortOrder.RATING_ASC,
SortOrder.NEWEST,
SortOrder.NEWEST_ASC,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.ADDED,
SortOrder.ADDED_ASC,
SortOrder.RELEVANCE,
)
override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = MangaSearchQueryCapabilities(
SearchCapability(
field = TAG,
criteriaTypes = setOf(Include::class, Exclude::class),
isMultiple = true,
),
SearchCapability(
field = TITLE_NAME,
criteriaTypes = setOf(Match::class),
isMultiple = false,
),
SearchCapability(
field = STATE,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = AUTHOR,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = CONTENT_TYPE,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = CONTENT_RATING,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = DEMOGRAPHIC,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = ORIGINAL_LANGUAGE,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = LANGUAGE,
criteriaTypes = setOf(Include::class),
isMultiple = true,
),
SearchCapability(
field = PUBLICATION_YEAR,
criteriaTypes = setOf(Match::class),
isMultiple = false,
),
)
override suspend fun getFilterOptions(): MangaListFilterOptions = coroutineScope {
val localesDeferred = async { fetchAvailableLocales() }
val tagsDeferred = async { fetchAvailableTags() }
MangaListFilterOptions(
availableTags = tagsDeferred.await(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
MangaState.ABANDONED,
),
availableContentRating = EnumSet.allOf(ContentRating::class.java),
availableDemographics = EnumSet.of(
Demographic.SHOUNEN,
Demographic.SHOUJO,
Demographic.SEINEN,
Demographic.JOSEI,
Demographic.NONE,
),
availableLocales = localesDeferred.await(),
)
}
private fun SearchableField.toParamName(): String = when (this) {
TITLE_NAME -> "title"
TAG -> "includedTags[]"
AUTHOR -> "authors[]"
STATE -> "status[]"
CONTENT_TYPE -> "contentType[]"
CONTENT_RATING -> "contentRating[]"
DEMOGRAPHIC -> "publicationDemographic[]"
ORIGINAL_LANGUAGE -> "originalLanguage[]"
LANGUAGE -> "availableTranslatedLanguage[]"
PUBLICATION_YEAR -> "year"
}
private fun Any?.toQueryParam(): String = when (this) {
is String -> urlEncoded()
is Locale -> if (language == "in") "id" else language
is MangaTag -> key
is MangaState -> when (this) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "cancelled"
MangaState.PAUSED -> "hiatus"
else -> ""
}
is ContentRating -> when (this) {
ContentRating.SAFE -> "safe"
// quick fix for double value
ContentRating.SUGGESTIVE -> "suggestive&contentRating[]=erotica"
ContentRating.ADULT -> "pornographic"
}
is Demographic -> when (this) {
Demographic.SHOUNEN -> "shounen"
Demographic.SHOUJO -> "shoujo"
Demographic.SEINEN -> "seinen"
Demographic.JOSEI -> "josei"
Demographic.NONE -> "none"
else -> ""
}
is SortOrder -> when (this) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.UPDATED_ASC -> "[latestUploadedChapter]=asc"
SortOrder.RATING -> "[rating]=desc"
SortOrder.RATING_ASC -> "[rating]=asc"
SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
SortOrder.NEWEST -> "[year]=desc"
SortOrder.NEWEST_ASC -> "[year]=asc"
SortOrder.POPULARITY -> "[followedCount]=desc"
SortOrder.POPULARITY_ASC -> "[followedCount]=asc"
SortOrder.ADDED -> "[createdAt]=desc"
SortOrder.ADDED_ASC -> "[createdAt]=asc"
SortOrder.RELEVANCE -> "&order[relevance]=desc"
else -> "[latestUploadedChapter]=desc"
}
else -> this.toString().urlEncoded()
}
private fun StringBuilder.appendCriterion(field: SearchableField, value: Any?, paramName: String? = null) {
val param = paramName ?: field.toParamName()
if (param.isNotBlank()) {
append("&$param=")
append(value.toQueryParam())
}
}
override suspend fun getList(query: MangaSearchQuery): List<Manga> {
val url = buildString {
append("https://api.$domain/manga?limit=$PAGE_SIZE&offset=${query.offset}")
.append("&includes[]=cover_art&includes[]=author&includes[]=artist&includedTagsMode=AND&excludedTagsMode=OR")
var hasContentRating = false
query.criteria.forEach { criterion ->
when (criterion) {
is Include<*> -> {
if (criterion.field == CONTENT_RATING) {
hasContentRating = true
}
criterion.values.forEach { appendCriterion(criterion.field, it) }
}
is Exclude<*> -> {
criterion.values.forEach { appendCriterion(criterion.field, it, "excludedTags[]") }
}
is Match<*> -> {
appendCriterion(criterion.field, criterion.value)
}
else -> {
// Not supported
}
}
}
// If contentRating is not provided, add default values
if (!hasContentRating) {
append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
}
append("&order")
append((query.order ?: defaultSortOrder).toQueryParam())
}
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
return json.mapJSON { jo -> jo.fetchManga(null) }
}
override suspend fun getDetails(manga: Manga): Manga {
val mangaId = manga.url.removePrefix("/")
return getDetails(mangaId)
}
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? {
val regex = Regex("[0-9a-f\\-]{10,}", RegexOption.IGNORE_CASE)
val mangaId = link.pathSegments.find { regex.matches(it) } ?: return null
return getDetails(mangaId)
}
private suspend fun getDetails(mangaId: String): Manga = coroutineScope {
val jsonDeferred = async {
webClient.httpGet(
"https://api.$domain/manga/${mangaId}?includes[]=artist&includes[]=author&includes[]=cover_art",
).parseJson().getJSONObject("data")
}
val feedDeferred = async { loadChapters(mangaId) }
jsonDeferred.await().fetchManga(mapChapters(feedDeferred.await()))
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val domain = domain
val chapterJson = webClient.httpGet("https://api.$domain/at-home/server/${chapter.url}?forcePort443=false")
.parseJson()
.getJSONObject("chapter")
val pages = chapterJson.getJSONArray("data")
val prefix = "https://uploads.$domain/data/${chapterJson.getString("hash")}/"
val json = webClient.httpGet(
"https://api.$domain/at-home/server/${chapter.url}?forcePort443=false",
).parseJson()
val chapterJson = json.getJSONObject("chapter")
val server = config[preferredServerKey] ?: SERVER_DATA
val pages = chapterJson.getJSONArray(
if (server == SERVER_DATA_SAVER) "dataSaver" else "data",
)
val prefix = "${json.getString("baseUrl")}/$server/${chapterJson.getString("hash")}/"
return List(pages.length()) { i ->
val url = prefix + pages.getString(i)
MangaPage(
@@ -159,19 +299,87 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val tags = webClient.httpGet("https://api.${domain}/manga/tag").parseJson()
.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,
)
}
}
private fun JSONObject.firstStringValue() = values().next() as String
private suspend fun fetchAvailableLocales(): 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.fetchManga(chapters: List<MangaChapter>?): Manga {
val id = getString("id")
val attrs = getJSONObject("attributes")
val relations = getJSONArray("relationships").associateByKey("type")
val cover = relations["cover_art"]
?.firstOrNull()
?.getJSONObject("attributes")
?.getString("fileName")
?.let {
"https://uploads.$domain/covers/$id/$it"
}
val authors: Set<String> = (relations["author"] ?: relations["artist"])
?.mapNotNullToSet {
it.getJSONObject("attributes")?.getStringOrNull("name")
}.orEmpty()
return Manga(
id = generateUid(id),
title = requireNotNull(attrs.getJSONObject("title").selectByLocale()) {
"Title should not be null"
},
altTitles = setOfNotNull(attrs.optJSONArray("altTitles")?.flatten()?.selectByLocale()), // TODO
url = id,
publicUrl = "https://$domain/title/$id",
rating = RATING_UNKNOWN,
contentRating = when (attrs.getStringOrNull("contentRating")) {
"pornographic" -> ContentRating.ADULT
"erotica", "suggestive" -> ContentRating.SUGGESTIVE
"safe" -> ContentRating.SAFE
else -> null
},
coverUrl = cover?.plus(".256.jpg"),
largeCoverUrl = cover,
description = attrs.optJSONObject("description")?.selectByLocale(),
tags = attrs.getJSONArray("tags").mapJSONToSet { tag ->
MangaTag(
title = tag.getJSONObject("attributes")
.getJSONObject("name")
.firstStringValue()
.toTitleCase(),
key = tag.getString("id"),
source = source,
)
},
state = when (attrs.getStringOrNull("status")) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"hiatus" -> MangaState.PAUSED
"cancelled" -> MangaState.ABANDONED
else -> null
},
authors = authors,
chapters = chapters,
source = source,
)
}
private fun JSONObject.firstStringValue() = entries<String>().first().value
private fun JSONObject.selectByLocale(): String? {
val preferredLocales = context.getPreferredLocales()
@@ -179,7 +387,20 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
getStringOrNull(locale.language)?.let { return it }
getStringOrNull(locale.toLanguageTag())?.let { return it }
}
return getStringOrNull(LOCALE_FALLBACK) ?: values().nextOrNull() as? String
return getStringOrNull(LOCALE_FALLBACK) ?: entries<String>().firstOrNull()?.value?.nullIfEmpty()
}
private fun JSONArray.flatten(): JSONObject {
val result = JSONObject()
repeat(length()) { i ->
val jo = optJSONObject(i)
if (jo != null) {
for (key in jo.keys()) {
result.put(key, jo.get(key))
}
}
}
return result
}
private suspend fun loadChapters(mangaId: String): List<JSONObject> {
@@ -220,13 +441,12 @@ 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") {
return Chapters(
data = json.optJSONArray("data")?.toJSONList().orEmpty(),
data = json.optJSONArray("data")?.asTypedList<JSONObject>().orEmpty(),
total = json.getInt("total"),
)
} else {
@@ -244,41 +464,52 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
Locale.ROOT,
)
val chaptersBuilder = ChaptersListBuilder(list.size)
val branchedChapters = HashMap<String?, HashMap<Float, MangaChapter>>()
val branchedChapters = HashMap<String?, HashMap<Pair<Int, Float>, MangaChapter>>()
for (jo in list) {
val id = jo.getString("id")
val attrs = jo.getJSONObject("attributes")
if (!attrs.isNull("externalUrl")) {
continue
}
val number = jo.getJSONObject("attributes").getFloatOrDefault("chapter", 0f)
val number = attrs.getFloatOrDefault("chapter", 0f)
val volume = attrs.getIntOrDefault("volume", 0)
val locale = attrs.getStringOrNull("translatedLanguage")?.let { Locale.forLanguageTag(it) }
val lc = locale?.getDisplayName(locale)?.toTitleCase(locale)
val relations = jo.getJSONArray("relationships").associateByKey("type")
val team = relations["scanlation_group"]?.getJSONObject("attributes")?.getStringOrNull("name")
?.takeUnless { it.isBlank() }
val team =
relations["scanlation_group"]?.firstOrNull()?.optJSONObject("attributes")?.getStringOrNull("name")
val branch = (list.indices).firstNotNullOf { i ->
val b = if (i == 0) lc else "$lc ($i)"
if (branchedChapters[b]?.get(number) == null) b else null
if (branchedChapters[b]?.get(volume to number) == null) b else null
}
val chapter = MangaChapter(
id = generateUid(id),
name = attrs.getStringOrNull("title")?.takeUnless(String::isEmpty)
?: "Chapter #${number.toString().removeSuffix(".0")}",
number = if (number <= 0f) (branchedChapters[branch]?.size?.plus(1) ?: 0) else number.toInt(),
title = attrs.getStringOrNull("title"),
number = number,
volume = volume,
url = id,
scanlator = team,
uploadDate = dateFormat.tryParse(attrs.getString("publishAt")),
uploadDate = dateFormat.parseSafe(attrs.getString("publishAt")),
branch = branch,
source = source,
)
if (chaptersBuilder.add(chapter)) {
branchedChapters.getOrPut(branch, ::HashMap)[number] = chapter
branchedChapters.getOrPut(branch, ::HashMap)[volume to number] = chapter
}
}
return chaptersBuilder.toList()
}
private fun JSONArray.associateByKey(key: String): Map<String, List<JSONObject>> {
val destination = LinkedHashMap<String, MutableList<JSONObject>>(length())
repeat(length()) { i ->
val item = getJSONObject(i)
val keyValue = item.getString(key)
destination.computeIfAbsent(keyValue) { mutableListOf() }.add(item)
}
return destination
}
private class Chapters(
val data: List<JSONObject>,
val total: Int,

View File

@@ -0,0 +1,493 @@
package org.koitharu.kotatsu.parsers.site.all
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.bitmap.Rect
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.min
private const val PIECE_SIZE = 200
private const val MIN_SPLIT_COUNT = 5
internal abstract class MangaFireParser(
context: MangaLoaderContext,
source: MangaParserSource,
private val siteLang: String,
) : PagedMangaParser(context, source, 30), Interceptor, MangaParserAuthProvider {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("mangafire.to")
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.RATING,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL,
SortOrder.RELEVANCE,
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val authUrl: String
get() = "https://${domain}"
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.value.contains("user")
}
}
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body()
return body.selectFirst("form.ajax input[name*=username]")?.attr("value")
?: body.parseFailed("Cannot find username")
}
private val tags = suspendLazy(soft = true) {
webClient.httpGet("https://$domain/filter").parseHtml()
.select(".genres > li").map {
MangaTag(
title = it.selectFirstOrThrow("label").ownText().toTitleCase(sourceLocale),
key = it.selectFirstOrThrow("input").attr("value"),
source = source,
)
}.associateBy { it.title }
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
MangaState.PAUSED,
MangaState.UPCOMING,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = "https://$domain/filter".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("language[]", siteLang)
when {
!filter.query.isNullOrEmpty() -> {
val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part ->
part.urlEncoded()
}
addEncodedQueryParameter("keyword", encodedQuery)
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
SortOrder.RELEVANCE -> "most_relevance"
else -> ""
},
)
}
else -> {
filter.tagsExclude.forEach { tag ->
addQueryParameter("genre[]", "-${tag.key}")
}
filter.tags.forEach { tag ->
addQueryParameter("genre[]", tag.key)
}
filter.locale?.let {
addQueryParameter("language[]", it.language)
}
filter.states.forEach { state ->
addQueryParameter(
name = "status[]",
value = when (state) {
MangaState.ONGOING -> "releasing"
MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "discontinued"
MangaState.PAUSED -> "on_hiatus"
MangaState.UPCOMING -> "info"
else -> throw IllegalArgumentException("$state not supported")
},
)
}
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
SortOrder.RELEVANCE -> "most_relevance"
else -> ""
},
)
}
}
}.build()
return webClient.httpGet(url)
.parseHtml().parseMangaList()
}
private fun Document.parseMangaList(): List<Manga> {
return select(".original.card-lg .unit .inner").map {
val a = it.selectFirstOrThrow(".info > a")
val mangaUrl = a.attrAsRelativeUrl("href")
Manga(
id = generateUid(mangaUrl),
url = mangaUrl,
publicUrl = mangaUrl.toAbsoluteUrl(domain),
title = a.ownText(),
coverUrl = it.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
source = source,
altTitles = emptySet(),
largeCoverUrl = null,
authors = emptySet(),
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val document = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val availableTags = tags.get()
var isAdult = false
var isSuggestive = false
val author = document.select("div.meta a[href*=/author/]")
.joinToString { it.ownText() }.nullIfEmpty()
return manga.copy(
title = document.selectFirstOrThrow(".info > h1").ownText(),
altTitles = setOfNotNull(document.selectFirst(".info > h6")?.ownTextOrNull()),
rating = document.selectFirst("div.rating-box")?.attr("data-score")
?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN,
coverUrl = document.selectFirstOrThrow("div.manga-detail div.poster img")
.attrAsAbsoluteUrl("src"),
tags = document.select("div.meta a[href*=/genre/]").mapNotNullToSet {
val tag = it.ownText()
if (tag == "Hentai") {
isAdult = true
} else if (tag == "Ecchi") {
isSuggestive = true
}
availableTags[tag.toTitleCase(sourceLocale)]
},
contentRating = when {
isAdult -> ContentRating.ADULT
isSuggestive -> ContentRating.SUGGESTIVE
else -> ContentRating.SAFE
},
state = document.selectFirst(".info > p")?.ownText()?.let {
when (it.lowercase()) {
"releasing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
"discontinued" -> MangaState.ABANDONED
"on_hiatus" -> MangaState.PAUSED
"info" -> MangaState.UPCOMING
else -> null
}
},
authors = setOfNotNull(author),
description = document.selectFirstOrThrow("#synopsis div.modal-content").html(),
chapters = getChapters(manga.url, document),
)
}
private data class ChapterBranch(
val type: String,
val langCode: String,
val langTitle: String,
)
private suspend fun getChapters(mangaUrl: String, document: Document): List<MangaChapter> {
val availableTypes = document.select(".chapvol-tab > a").map {
it.attr("data-name")
}
val langTypePairs = document.select(".m-list div.tab-content").flatMap {
val type = it.attr("data-name")
it.select(".list-menu .dropdown-item").map { item ->
ChapterBranch(
type = type,
langCode = item.attr("data-code").lowercase(),
langTitle = item.attr("data-title"),
)
}
}.filter {
it.langCode == siteLang && availableTypes.contains(it.type)
}
val id = mangaUrl.substringAfterLast('.')
return coroutineScope {
langTypePairs.map {
async {
getChaptersBranch(id, it)
}
}.awaitAll().flatten()
}
}
private suspend fun getChaptersBranch(mangaId: String, branch: ChapterBranch): List<MangaChapter> {
val chapterElements = webClient
.httpGet("https://$domain/ajax/read/$mangaId/${branch.type}/${branch.langCode}")
.parseJson()
.getJSONObject("result")
.getString("html")
.let(Jsoup::parseBodyFragment)
.select("ul li a")
if (branch.type == "chapter") {
val doc = webClient
.httpGet("https://$domain/ajax/manga/$mangaId/${branch.type}/${branch.langCode}")
.parseJson()
.getString("result")
.let(Jsoup::parseBodyFragment)
doc.select("ul li a").withIndex().forEach { (i, it) ->
val date = it.select("span")[1].ownText()
chapterElements[i].attr("upload-date", date)
chapterElements[i].attr("other-title", it.attr("title"))
}
}
return chapterElements.mapChapters(reversed = true) { _, it ->
MangaChapter(
id = generateUid(it.attr("href")),
title = it.attr("title").ifBlank {
"${branch.type.toTitleCase()} ${it.attr("data-number")}"
},
number = it.attr("data-number").toFloat(),
volume = it.attr("other-title").let {
volumeNumRegex.find(it)?.groupValues?.getOrNull(2)?.toInt() ?: 0
},
url = "${branch.type}/${it.attr("data-id")}",
scanlator = null,
uploadDate = dateFormat.parseSafe(it.attr("upload-date")),
branch = "${branch.langTitle} ${branch.type.toTitleCase()}",
source = source,
)
}
}
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
private val volumeNumRegex = Regex("""vol(ume)?\s*(\d+)""", RegexOption.IGNORE_CASE)
override suspend fun getRelatedManga(seed: Manga): List<Manga> = coroutineScope {
val document = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()
val total = document.select(
"section.m-related a[href*=/manga/], .side-manga:not(:has(.head:contains(trending))) .unit",
).size
val mangas = ArrayList<Manga>(total)
// "Related Manga"
document.select("section.m-related a[href*=/manga/]").map {
async {
val url = it.attrAsRelativeUrl("href")
val mangaDocument = webClient
.httpGet(url.toAbsoluteUrl(domain))
.parseHtml()
val chaptersInManga = mangaDocument.select(".m-list div.tab-content .list-menu .dropdown-item")
.map { it.attr("data-code").lowercase() }
if (!chaptersInManga.contains(siteLang)) {
return@async null
}
Manga(
id = generateUid(url),
url = url,
publicUrl = url.toAbsoluteUrl(domain),
title = it.ownText(),
coverUrl = mangaDocument.selectFirstOrThrow("div.manga-detail div.poster img")
.attrAsAbsoluteUrl("src"),
source = source,
altTitles = emptySet(),
largeCoverUrl = null,
authors = emptySet(),
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
)
}
}.awaitAll()
.filterNotNullTo(mangas)
// "You may also like"
document.select(".side-manga:not(:has(.head:contains(trending))) .unit").forEach {
val url = it.attrAsRelativeUrl("href")
mangas.add(
Manga(
id = generateUid(url),
url = url,
publicUrl = url.toAbsoluteUrl(domain),
title = it.selectFirstOrThrow(".info h6").ownText(),
coverUrl = it.selectFirstOrThrow(".poster img").attrAsAbsoluteUrl("src"),
source = source,
altTitles = emptySet(),
largeCoverUrl = null,
authors = emptySet(),
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
),
)
}
mangas.ifEmpty {
// fallback: author's other works
document.select("div.meta a[href*=/author/]").map {
async {
val url = it.attrAsAbsoluteUrl("href").toHttpUrl()
.newBuilder()
.addQueryParameter("language[]", siteLang)
.build()
webClient.httpGet(url)
.parseHtml().parseMangaList()
}
}.awaitAll().flatten()
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val images = webClient
.httpGet("https://$domain/ajax/read/${chapter.url}")
.parseJson()
.getJSONObject("result")
.getJSONArray("images")
val pages = ArrayList<MangaPage>(images.length())
for (i in 0 until images.length()) {
val img = images.getJSONArray(i)
val url = img.getString(0)
val offset = img.getInt(2)
pages.add(
MangaPage(
id = generateUid(url),
url = if (offset < 1) {
url
} else {
"$url#scrambled_$offset"
},
preview = null,
source = source,
),
)
}
return pages
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (request.url.fragment?.startsWith("scrambled") != true) {
return response
}
return context.redrawImageResponse(response) { bitmap ->
val offset = request.url.fragment!!.substringAfter("_").toInt()
val width = bitmap.width
val height = bitmap.height
val result = context.createBitmap(width, height)
val pieceWidth = min(PIECE_SIZE, width.ceilDiv(MIN_SPLIT_COUNT))
val pieceHeight = min(PIECE_SIZE, height.ceilDiv(MIN_SPLIT_COUNT))
val xMax = width.ceilDiv(pieceWidth) - 1
val yMax = height.ceilDiv(pieceHeight) - 1
for (y in 0..yMax) {
for (x in 0..xMax) {
val xDst = pieceWidth * x
val yDst = pieceHeight * y
val w = min(pieceWidth, width - xDst)
val h = min(pieceHeight, height - yDst)
val xSrc = pieceWidth * when (x) {
xMax -> x // margin
else -> (xMax - x + offset) % xMax
}
val ySrc = pieceHeight * when (y) {
yMax -> y // margin
else -> (yMax - y + offset) % yMax
}
val srcRect = Rect(xSrc, ySrc, xSrc + w, ySrc + h)
val dstRect = Rect(xDst, yDst, xDst + w, yDst + h)
result.drawBitmap(bitmap, srcRect, dstRect)
}
}
result
}
}
private fun Int.ceilDiv(other: Int) = (this + (other - 1)) / other
@MangaSourceParser("MANGAFIRE_EN", "MangaFire English", "en")
class English(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_EN, "en")
@MangaSourceParser("MANGAFIRE_ES", "MangaFire Spanish", "es")
class Spanish(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_ES, "es")
@MangaSourceParser("MANGAFIRE_ESLA", "MangaFire Spanish (Latim)", "es")
class SpanishLatim(context: MangaLoaderContext) :
MangaFireParser(context, MangaParserSource.MANGAFIRE_ESLA, "es-la")
@MangaSourceParser("MANGAFIRE_FR", "MangaFire French", "fr")
class French(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_FR, "fr")
@MangaSourceParser("MANGAFIRE_JA", "MangaFire Japanese", "ja")
class Japanese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_JA, "ja")
@MangaSourceParser("MANGAFIRE_PT", "MangaFire Portuguese", "pt")
class Portuguese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_PT, "pt")
@MangaSourceParser("MANGAFIRE_PTBR", "MangaFire Portuguese (Brazil)", "pt")
class PortugueseBR(context: MangaLoaderContext) :
MangaFireParser(context, MangaParserSource.MANGAFIRE_PTBR, "pt-br")
}

View File

@@ -0,0 +1,316 @@
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.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGAPARK", "MangaPark")
internal class MangaPark(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANGAPARK, pageSize = 36) {
override val configKeyDomain = ConfigKey.Domain(
"mangapark.net",
"mangapark.com",
"mangapark.org",
"mangapark.me",
"mangapark.io",
"mangapark.to",
"comicpark.org",
"comicpark.to",
"readpark.org",
"readpark.net",
"parkmanga.com",
"parkmanga.net",
"parkmanga.org",
"mpark.to",
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
isOriginalLocaleSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tagsMap.get().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
MangaState.PAUSED,
MangaState.UPCOMING,
),
availableContentRating = EnumSet.of(ContentRating.SAFE),
availableLocales = 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"),
),
)
init {
context.cookieJar.insertCookies(domain, "nsfw", "2")
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/search?page=")
append(page.toString())
filter.query?.let {
append("&word=")
append(filter.query.urlEncoded())
}
append("&genres=")
filter.tags.joinTo(this, ",") { it.key }
append("|")
filter.tagsExclude.joinTo(this, ",") { 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"
else -> throw IllegalArgumentException("$it not supported")
},
)
}
append("&sortby=")
append(
when (order) {
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)
}
filter.originalLocale?.let {
append("&orig=")
append(it.language)
}
}
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(),
title = div.selectFirst("h3")?.text().orEmpty(),
altTitles = emptySet(),
rating = div.selectFirst("span.text-yellow-500")?.text()?.toFloatOrNull()?.div(10F) ?: RATING_UNKNOWN,
tags = emptySet(),
authors = emptySet(),
state = null,
source = source,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
)
}
}
private val tagsMap = suspendLazy(initializer = ::parseTags)
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 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)
val author = doc.selectFirst("div[q:key=tz_4]")?.textOrNull()
manga.copy(
altTitles = setOfNotNull(doc.selectFirst("div[q:key=tz_2]")?.textOrNull()),
authors = setOfNotNull(author),
description = doc.selectFirst("react-island[q:key=0a_9]")?.html(),
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,
contentRating = if (nsfw) ContentRating.ADULT else ContentRating.SAFE,
chapters = doc.body().select("div.group.flex div.px-2").mapChapters(reversed = true) { 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),
title = a.textOrNull(),
number = i + 1f,
volume = 0,
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 {
WordSet(" ago").endsWith(d) -> {
parseRelativeDate(d)
}
WordSet("just now").startsWith(d) -> {
Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
else -> dateFormat.parseSafe(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 id = chapter.url.removeSuffix('/').substringAfterLast('/').substringBefore('-')
val s = doc.selectFirstOrThrow("script:containsData($id)").data()
val script = if (s.contains("\"comic-")) {
s.substringAfterLast("\"comic-")
} else {
s.substringAfterLast("\"manga-")
}
return Regex("\"(https?:.+?)\"")
.findAll(script)
.mapIndexedNotNullTo(ArrayList()) { i, it ->
val url = it.groupValues.getOrNull(1) ?: return@mapIndexedNotNullTo null
if (url.contains(".jpg") || url.contains(".jpeg") || url.contains(".jfif") || url.contains(".pjpeg") ||
url.contains(".pjp") || url.contains(".png") || url.contains(".webp") || url.contains(".avif") ||
url.contains(".gif")
) {
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
} else {
return@mapIndexedNotNullTo null
}
}
}
}

View File

@@ -0,0 +1,332 @@
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.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.asTypedList
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.suspendlazy.suspendLazy
import java.util.*
internal abstract class MangaPlusParser(
context: MangaLoaderContext,
source: MangaParserSource,
private val sourceLang: String,
) : SinglePageMangaParser(context, source), Interceptor {
private val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions()
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
return when {
filter.query.isNullOrEmpty() -> {
when (order) {
SortOrder.POPULARITY -> getPopularList()
SortOrder.UPDATED -> getLatestList()
else -> getAllTitleList()
}
}
else -> getAllTitleList(filter.query)
}
}
private suspend fun getPopularList(): List<Manga> {
val json = apiCall("/title_list/ranking")
return json.getJSONObject("titleRankingView")
.getJSONArray("titles")
.asTypedList<JSONObject>()
.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").asTypedList<JSONObject>() }
.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"),
altTitles = emptySet(),
authors = setOf(author),
contentRating = null,
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
val author = title.getString("author")
.split("/").joinToString(transform = String::trim)
return manga.copy(
title = title.getString("name"),
publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain),
coverUrl = title.getString("portraitImageUrl"),
authors = setOf(author),
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
.asTypedList<JSONObject>()
.flatMap {
it.optJSONArray("firstChapterList")?.asTypedList<JSONObject>().orEmpty() +
it.optJSONArray("lastChapterList")?.asTypedList<JSONObject>().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,
title = subtitle,
number = chapter.getString("name")
.substringAfter("#")
.toFloatOrNull() ?: -1f,
volume = 0,
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
}
return response.map { responseBody ->
val contentType = response.headers["Content-Type"] ?: "image/jpeg"
val image = responseBody.bytes().decodeXorCipher(encryptionKey)
image.toResponseBody(contentType.toMediaTypeOrNull())
}
}
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")
.asTypedList<JSONObject>()
.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,
MangaParserSource.MANGAPLUSPARSER_EN,
"ENGLISH",
)
@MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es")
class Spanish(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_ES,
"SPANISH",
)
@MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr")
class French(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_FR,
"FRENCH",
)
@MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id")
class Indonesian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_ID,
"INDONESIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
class Portuguese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_PTBR,
"PORTUGUESE_BR",
)
@MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru")
class Russian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_RU,
"RUSSIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th")
class Thai(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_TH,
"THAI",
)
@MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi")
class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_VI,
"VIETNAMESE",
)
@MangaSourceParser("MANGAPLUSPARSER_DE", "MANGA Plus German", "de")
class German(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaParserSource.MANGAPLUSPARSER_DE,
"GERMAN",
)
}

View File

@@ -0,0 +1,390 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.MutableIntObjectMap
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.bitmap.Rect
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
@MangaSourceParser("MANGAREADERTO", "MangaReader.To")
internal class MangaReaderToParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANGAREADERTO, 16),
Interceptor, MangaParserAuthProvider {
override val configKeyDomain = ConfigKey.Domain("mangareader.to")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val authUrl: String
get() = "https://${domain}/home"
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name.contains("connect.sid")
}
}
// It will be easier to connect to a manga page, as the source redirects to a lot of advertising.
override suspend fun getUsername(): String {
val body = webClient.httpGet("https://${domain}/user/profile").parseHtml().body()
return body.getElementById("pro5-name")?.attr("value") ?: body.parseFailed("Cannot find username")
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.RATING,
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL,
)
val tags = suspendLazy(soft = true) {
val document = webClient.httpGet("https://$domain/filter").parseHtml()
document.select("div.f-genre-item").map {
MangaTag(
title = it.ownText().toTitleCase(sourceLocale),
key = it.attr("data-id"),
source = source,
)
}.associateBy { it.title }
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.ABANDONED,
MangaState.PAUSED,
MangaState.UPCOMING,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = "https://$domain".toHttpUrl().newBuilder().apply {
when {
!filter.query.isNullOrEmpty() -> {
addPathSegment("search")
addQueryParameter("keyword", filter.query)
addQueryParameter("page", page.toString())
}
else -> {
addPathSegment("filter")
addQueryParameter("page", page.toString())
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.POPULARITY -> "most-viewed"
SortOrder.RATING -> "score"
SortOrder.UPDATED -> "latest-updated"
SortOrder.NEWEST -> "release-date"
SortOrder.ALPHABETICAL -> "name-az"
else -> "default"
},
)
addQueryParameter("genres", filter.tags.joinToString(",") { it.key })
addQueryParameter(
name = "status",
value = when (val state = filter.states.oneOrThrowIfMany()) {
MangaState.ONGOING -> "2"
MangaState.FINISHED -> "1"
MangaState.ABANDONED -> "4"
MangaState.PAUSED -> "3"
MangaState.UPCOMING -> "5"
null -> ""
else -> throw IllegalArgumentException("$state not supported")
},
)
}
}
}.build()
val document = webClient.httpGet(url).parseHtml()
return document.select(".manga_list-sbs .manga-poster").map {
val mangaUrl = it.attrAsRelativeUrl("href")
val thumb = it.select("img")
Manga(
id = generateUid(mangaUrl),
url = mangaUrl,
publicUrl = mangaUrl.toAbsoluteUrl(domain),
title = thumb.attr("alt"),
coverUrl = thumb.attr("src"),
source = source,
altTitles = emptySet(),
authors = emptySet(),
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
)
}
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val document = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()
return document.select(".block_area_authors-other .manga_list-sbs .manga-poster, .featured-block-ul .manga-poster")
.map {
val mangaUrl = it.attrAsRelativeUrl("href")
val thumb = it.selectFirstOrThrow("img")
Manga(
id = generateUid(mangaUrl),
url = mangaUrl,
publicUrl = mangaUrl.toAbsoluteUrl(domain),
title = thumb.attr("alt"),
coverUrl = thumb.attrAsAbsoluteUrlOrNull("src"),
source = source,
altTitles = emptySet(),
authors = emptySet(),
contentRating = null,
rating = RATING_UNKNOWN,
state = null,
tags = emptySet(),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val document = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val availableTags = tags.get()
var isAdult = false
var isSuggestive = false
val author = document.select("div.anisc-info a[href*=/author/]")
.joinToString { it.ownText().replace(", ", " ") }.nullIfEmpty()
return manga.copy(
title = document.selectFirst("h2.manga-name")!!.ownText(),
altTitles = setOfNotNull(document.selectFirst("div.manga-name-or")?.ownTextOrNull()),
rating = document.selectFirst("div.anisc-info .item:contains(score:) > .name")
?.text()?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN,
coverUrl = document.selectFirst(".manga-poster > img")?.attrAsAbsoluteUrlOrNull("src"),
tags = document.select("div.genres > a[href*=/genre/]").mapNotNullToSet {
val tag = it.ownText()
if (tag == "Hentai") {
isAdult = true
} else if (tag == "Ecchi") {
isSuggestive = true
}
availableTags[tag]
},
contentRating = when {
isAdult -> ContentRating.ADULT
isSuggestive -> ContentRating.SUGGESTIVE
else -> ContentRating.SAFE
},
state = document.selectFirst("div.anisc-info .item:contains(status:) > .name")
?.text()?.let {
when (it) {
"Publishing" -> MangaState.ONGOING
"Finished" -> MangaState.FINISHED
"On Hiatus" -> MangaState.PAUSED
"Discontinued" -> MangaState.ABANDONED
"Not yet published" -> MangaState.UPCOMING
else -> null
}
},
authors = setOfNotNull(author),
description = document.select("div.description").html(),
chapters = parseChapters(document),
source = source,
)
}
private fun parseChapters(document: Document): List<MangaChapter> {
val total =
document.select(".chapters-list-ul > ul > li.chapter-item, .volume-list-ul div.lang-volumes > div.item").size
val chapters = ChaptersListBuilder(total)
document.select(".chapters-list-ul > ul").forEach { ul ->
ul.select("li.chapter-item").reversed().forEach { li ->
val a = li.selectFirst("a")!!
chapters.add(
MangaChapter(
id = generateUid(a.attrAsRelativeUrl("href")),
title = a.attrOrNull("title"),
number = li.attr("data-number").toFloat(),
volume = 0,
url = a.attrAsRelativeUrl("href"),
scanlator = null,
uploadDate = 0L,
branch = createBranchName(ul.id().substringBefore("-chapters"), "Chapters"),
source = source,
),
)
}
}
val numRegex = Regex("""(\d+)""")
document.select(".volume-list-ul div.lang-volumes").forEach { div ->
div.select("div.item > div.manga-poster").reversed().forEach { vol ->
val url = vol.selectFirst("a")!!.attrAsRelativeUrl("href")
val name = vol.selectFirst("span")!!.ownText()
chapters.add(
MangaChapter(
id = generateUid(url),
title = name,
number = numRegex.find(name)?.groupValues?.getOrNull(1)?.toFloatOrNull() ?: 0f,
volume = 0,
url = url,
scanlator = null,
uploadDate = 0L,
branch = createBranchName(div.id().substringBefore("-volumes"), "Volumes"),
source = source,
),
)
}
}
return chapters.toList()
}
private fun createBranchName(lang: String, type: String): String {
val langCode = lang.substringBefore("-")
return Locale(langCode).displayLanguage + " " + type
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val typeAndId = webClient.httpGet(chapter.url.toAbsoluteUrl(domain))
.parseHtml()
.selectFirst("#wrapper")!!.run {
"${attr("data-reading-by")}/${attr("data-reading-id")}"
}
val document = webClient.httpGet("https://$domain/ajax/image/list/$typeAndId?quality=high")
.parseJson()
.getString("html")
.let(Jsoup::parse)
return document.select(".iv-card").map {
val url = it.attr("data-url")
MangaPage(
id = generateUid(url),
url = if (it.hasClass("shuffled")) {
"$url#scrambled"
} else {
url
},
preview = null,
source = source,
)
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (request.url.fragment != "scrambled") return response
return context.redrawImageResponse(response, ::descramble)
}
private val memo = MutableIntObjectMap<IntArray>()
private fun descramble(bitmap: Bitmap): Bitmap = synchronized(memo) {
val width = bitmap.width
val height = bitmap.height
val result = context.createBitmap(width, height)
val pieces = ArrayList<Piece>()
for (y in 0 until height step PIECE_SIZE) {
for (x in 0 until width step PIECE_SIZE) {
val w = min(PIECE_SIZE, width - x)
val h = min(PIECE_SIZE, height - y)
pieces.add(Piece(x, y, w, h))
}
}
val groups = pieces.groupBy { it.w shl 16 or it.h }
for (group in groups.values) {
val size = group.size
val permutation = memo.getOrPut(size) {
val random = SeedRandom("staystay")
// https://github.com/webcaetano/shuffle-seed
val indices = (0 until size).toMutableList()
IntArray(size) { indices.removeAt((random.nextDouble() * indices.size).toInt()) }
}
for ((i, original) in permutation.withIndex()) {
val src = group[i]
val dst = group[original]
val srcRect = Rect(src.x, src.y, src.x + src.w, src.y + src.h)
val dstRect = Rect(dst.x, dst.y, dst.x + dst.w, dst.y + dst.h)
result.drawBitmap(bitmap, srcRect, dstRect)
}
}
return result
}
private class Piece(val x: Int, val y: Int, val w: Int, val h: Int)
// https://github.com/davidbau/seedrandom
private class SeedRandom(key: String) {
private val input = ByteArray(RC4_WIDTH)
private val buffer = ByteArray(RC4_WIDTH)
private var pos = RC4_WIDTH
private val rc4 = Cipher.getInstance("RC4").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.toByteArray(), "RC4"))
update(input, 0, RC4_WIDTH, buffer) // RC4-drop[256]
}
fun nextDouble(): Double {
var num = nextByte()
var exp = 8
while (num < 1L shl 52) {
num = num shl 8 or nextByte()
exp += 8
}
while (num >= 1L shl 53) {
num = num ushr 1
exp--
}
return Math.scalb(num.toDouble(), -exp)
}
private fun nextByte(): Long {
if (pos == RC4_WIDTH) {
rc4.update(input, 0, RC4_WIDTH, buffer)
pos = 0
}
return buffer[pos++].toLong() and 0xFF
}
}
}
private const val RC4_WIDTH = 256
private const val PIECE_SIZE = 200

View File

@@ -0,0 +1,228 @@
package org.koitharu.kotatsu.parsers.site.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MANHWA210", "Manhwa210", type = ContentType.MANHWA)
internal class Manhwa210(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANHWA210, 60) {
override val configKeyDomain = ConfigKey.Domain("manhwa210.com")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.POPULARITY,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = availableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!filter.query.isNullOrEmpty() -> {
append("/search")
append("?filter[name]=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
}
append("&sort=")
append(
when (order) {
SortOrder.POPULARITY -> "-views"
SortOrder.UPDATED -> "-updated_at"
SortOrder.NEWEST -> "-created_at"
SortOrder.ALPHABETICAL -> "name"
SortOrder.ALPHABETICAL_DESC -> "-name"
else -> "-updated_at"
},
)
}
filter.tags.isNotEmpty() -> {
val tag = filter.tags.first()
append("/genre/")
append(tag.key)
append("?page=")
append(page)
}
else -> {
append("/list")
append("?sort=")
append(
when (order) {
SortOrder.POPULARITY -> "-views"
SortOrder.UPDATED -> "-updated_at"
SortOrder.NEWEST -> "-created_at"
SortOrder.ALPHABETICAL -> "name"
SortOrder.ALPHABETICAL_DESC -> "-name"
else -> "-updated_at"
},
)
append("&page=")
append(page)
}
}
if (filter.query.isNullOrEmpty()) {
append("&sort=")
when (order) {
SortOrder.POPULARITY -> append("-views")
SortOrder.UPDATED -> append("-updated_at")
SortOrder.NEWEST -> append("-created_at")
SortOrder.ALPHABETICAL -> append("name")
SortOrder.ALPHABETICAL_DESC -> append("-name")
else -> append("-updated_at")
}
}
if (filter.states.isNotEmpty()) {
append("&filter[status]=")
filter.states.forEach {
append(
when (it) {
MangaState.ONGOING -> "2,"
MangaState.FINISHED -> "1,"
else -> "1,2"
},
)
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid div.relative").map { div ->
val href = div.selectFirst("a[href^=/manga/]")?.attrOrNull("href")
?: div.parseFailed("Cant find manga image!")
val coverUrl = div.selectFirst("div.cover")?.attr("style")
?.substringAfter("url('")?.substringBefore("')")
Manga(
id = generateUid(href),
title = div.select("div.p-2 a.text-ellipsis").text(),
altTitles = emptySet(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = coverUrl.orEmpty(),
tags = setOf(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val author = root.selectFirst("div.mt-2:contains(Artist) span a")?.textOrNull()
return manga.copy(
altTitles = setOfNotNull(root.selectLast("div.grow div:contains(Alt name) span")?.textOrNull()),
state = when (root.selectFirst("div.mt-2:contains(Status) span.text-blue-500")?.text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
else -> null
},
tags = root.select("div.mt-2:contains(Genres) a.bg-gray-500").mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
},
authors = setOfNotNull(author),
description = root.selectFirst("meta[name=description]")?.attrOrNull("content"),
chapters = root.select("div.justify-between ul.overflow-y-auto.overflow-x-hidden a")
.mapChapters(reversed = true) { i, a ->
val href = a.attrAsRelativeUrl("href")
val name = a.selectFirst("span.text-ellipsis")?.text().orEmpty()
val dateText = a.parent()?.selectFirst("span.timeago")?.attr("datetime").orEmpty()
MangaChapter(
id = generateUid(href),
title = name,
number = i.toFloat(),
volume = 0,
url = href,
scanlator = null,
uploadDate = parseDateTime(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()
return doc.select("div.text-center img.lazy").mapNotNull { img ->
val url = img.requireSrc()
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
private fun parseDateTime(dateStr: String): Long = runCatching {
val parts = dateStr.split(' ')
val dateParts = parts[0].split('-')
val timeParts = parts[1].split(':')
val calendar = Calendar.getInstance()
calendar.set(
dateParts[0].toInt(),
dateParts[1].toInt() - 1,
dateParts[2].toInt(),
timeParts[0].toInt(),
timeParts[1].toInt(),
timeParts[2].toInt(),
)
calendar.timeInMillis
}.getOrDefault(0L)
private suspend fun availableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain").parseHtml()
return doc.select("ul.grid.grid-cols-2 a").mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text(),
source = source,
)
}
}
}

View File

@@ -0,0 +1,148 @@
package org.koitharu.kotatsu.parsers.site.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MISSKON", "MissKon", type = ContentType.OTHER)
internal class Misskon(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MISSKON, 24) {
override val configKeyDomain = ConfigKey.Domain("misskon.com")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( isSearchSupported = true )
override suspend fun getFilterOptions() = MangaListFilterOptions()
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!filter.query.isNullOrEmpty() -> {
append("/page/$page/")
append("?s=")
append(filter.query.urlEncoded())
}
order == SortOrder.POPULARITY -> {
append("/top3/")
}
else -> {
append("/page/$page")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("article.item-list").map { article ->
val titleEl = article.selectFirst(".post-box-title")!!
val href = titleEl.selectFirst("a")?.attrAsRelativeUrl("href")
?: article.parseFailed("Cannot find manga link")
Manga(
id = generateUid(href),
title = titleEl.text(),
altTitles = emptySet(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = article.selectFirst(".post-thumbnail img")?.absUrl("data-src").orEmpty(),
tags = setOf(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val postInnerEl = doc.selectFirst("article > .post-inner")!!
return manga.copy(
tags = postInnerEl.select(".post-tag > a").mapToSet { a ->
MangaTag(
key = a.text().lowercase(),
title = a.text(),
source = source
)
},
chapters = listOf(
MangaChapter(
id = manga.id,
title = "Oneshot", // 1 album, idk
number = 1f,
volume = 0,
url = manga.url,
scanlator = null,
uploadDate = 0L,
branch = null,
source = source
)
)
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val basePageUrl = doc.selectFirst("link[rel=canonical]")?.absUrl("href")
?: chapter.url.toAbsoluteUrl(domain)
val pages = mutableListOf<MangaPage>()
val pageLinks = doc.select("div.post-inner div.page-link:nth-child(1) .post-page-numbers")
if (pageLinks.isEmpty()) {
// Single page gallery
return doc.select("div.post-inner > div.entry > p > img")
.mapNotNull { img -> img.absUrl("data-src") }
.mapIndexed { i, url ->
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source
)
}
}
// Multi-page gallery
pageLinks.forEachIndexed { index, pageEl ->
val pageDoc = when (index) {
0 -> doc
else -> {
val url = "$basePageUrl${pageEl.text()}/"
webClient.httpGet(url).parseHtml()
}
}
pages.addAll(
pageDoc.select("div.post-inner > div.entry > p > img")
.mapNotNull { img -> img.absUrl("data-src") }
.map { url ->
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source
)
}
)
}
return pages
}
}

View File

@@ -0,0 +1,229 @@
package org.koitharu.kotatsu.parsers.site.all
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MULTPORN", "Multporn")
internal class Multporn(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MULTPORN, 42) {
override val configKeyDomain = ConfigKey.Domain("multporn.net")
override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.build()
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.NEWEST_ASC,
SortOrder.UPDATED,
SortOrder.UPDATED_ASC,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
init {
setFirstPage(0)
}
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableLocales = setOf(
Locale("en"),
Locale("de"),
Locale("ru"),
Locale("zh"),
Locale("es"),
),
availableContentTypes = EnumSet.of(
ContentType.COMICS,
ContentType.HENTAI,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!filter.query.isNullOrEmpty() -> {
append("/search?search_api_views_fulltext=")
val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part ->
part.urlEncoded()
}
append(encodedQuery)
append("&undefined=Search")
append("&page=$page")
}
filter.tags.isNotEmpty() -> {
val tag = filter.tags.first()
append("/category/")
append(tag.key)
append("?sort_by=")
append(
when (order) {
SortOrder.NEWEST -> "created"
else -> "title" // default
}
)
append("&page=0,")
append(page)
}
else -> {
append("/new")
append("?type=")
if (filter.types.isNotEmpty()) {
filter.types.oneOrThrowIfMany()?.let {
append(
when (it) {
ContentType.COMICS -> "1"
ContentType.HENTAI -> "2"
else -> "All" // all
},
)
}
} else append("All")
filter.locale?.let {
append("&language=")
append(
when (it) {
Locale("en") -> "1"
Locale("de") -> "2"
Locale("ru") -> "3"
Locale("zh") -> "4"
Locale("es") -> "5"
else -> "All"
}
)
}
append("&field_user_discription_value=All")
append("&sort_by=")
append(
when (order) {
SortOrder.NEWEST -> "created&sort_order=DESC"
SortOrder.NEWEST_ASC -> "created&sort_order=ASC"
SortOrder.UPDATED -> "changed&sort_order=DESC"
SortOrder.UPDATED_ASC -> "changed&sort_order=ASC"
else -> "created&sort_order=DESC" // default
}
)
append("&undefined=Apply")
append("&page=$page")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".masonry-item").map { div ->
val href = div.selectFirstOrThrow(".views-field-title a").attrAsRelativeUrl("href")
val coverUrl = div.selectFirstOrThrow(".views-field img").requireSrc()
Manga(
id = generateUid(href),
title = div.select(".views-field-title").text(),
altTitles = emptySet(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = coverUrl,
tags = emptySet(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val authors = (doc.select(".field:has(.field-label:contains(Author:)) .links a").map { it.text() } +
parseUnlabelledAuthorNames(doc)).distinct()
val tags = listOf("Tags", "Section", "Characters")
.flatMap { type ->
doc.select(".field:has(.field-label:contains($type:)) .links a").map { it.text() }
}
.distinct()
.map { tag ->
MangaTag(
title = tag,
key = tag.lowercase().replace(" ", "_"),
source = source,
)
}.toSet()
val isOngoing = doc.select(".field .links a").any { it.text() == "Ongoings" }
return manga.copy(
authors = authors.toSet(),
tags = tags,
description = buildString {
append("Pages: ")
append(doc.select(".jb-image img").size)
append("\n\n")
doc.select(".field:has(.field-label:contains(Section:)) .links a").joinTo(this, prefix = "Section: ") { it.text() }
doc.select(".field:has(.field-label:contains(Characters:)) .links a").joinTo(this, prefix = "\n\nCharacters: ") { it.text() }
},
state = if (isOngoing) MangaState.ONGOING else MangaState.FINISHED,
chapters = listOf(
MangaChapter(
id = generateUid(manga.url),
title = "Oneshot",
number = 1f,
volume = 0,
url = manga.url,
scanlator = null,
uploadDate = 0L,
branch = null,
source = source,
)
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.select(".jb-image img").mapIndexed { i, img ->
val url = img.attrAsAbsoluteUrl("src")
.replace("/styles/juicebox_2k/public", "")
.substringBefore("?")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
private fun parseUnlabelledAuthorNames(document: org.jsoup.nodes.Document): List<String> {
val authorClasses = listOf(
"field-name-field-author",
"field-name-field-authors-gr",
"field-name-field-img-group",
"field-name-field-hentai-img-group",
"field-name-field-rule-63-section"
)
return authorClasses.flatMap { className ->
document.select(".$className a").map { it.text().trim() }
}
}
}

View File

@@ -0,0 +1,451 @@
package org.koitharu.kotatsu.parsers.site.all
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.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
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.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
import java.util.regex.Pattern
@MangaSourceParser("MYREADINGMANGA", "MyReadingManga", type = ContentType.HENTAI)
internal class MyReadingManga(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MYREADINGMANGA, 18) {
override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
isOriginalLocaleSupported = true,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchTags(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
),
availableContentRating = EnumSet.of(ContentRating.ADULT),
availableLocales = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale.GERMAN,
Locale.ITALIAN,
Locale.KOREAN,
Locale.TRADITIONAL_CHINESE,
Locale("es"), // Spanish
Locale("pt"), // Portuguese
Locale("ru"), // Russian
Locale("tr"), // Turkish
Locale("vi"), // Vietnamese
Locale("ar"), // Arabic
Locale("id"), // Indonesian (Bahasa)
Locale("th"), // Thai
Locale("pl"), // Polish
Locale("sv"), // Swedish
Locale("nl"), // Dutch (Flemish Dutch)
Locale("hu"), // Hungarian
Locale("hi"), // Hindi
Locale("he"), // Hebrew
Locale("el"), // Greek
Locale("fi"), // Finnish
Locale("fil"), // Filipino
Locale("da"), // Danish
Locale("cs"), // Czech
Locale("hr"), // Croatian
Locale("bg"), // Bulgarian
Locale("zh", "HK"), // Cantonese
Locale("fa"), // Persian
Locale("sk"), // Slovak
Locale("ro"), // Romanian
Locale("no"), // Norwegian
Locale("ms"), // Malay
Locale("lt"), // Lithuanian
),
)
private fun getLanguageSlug(locale: Locale?): String? {
return when {
locale?.language == "fr" -> "french"
locale?.language == "ja" -> "jp"
locale?.language == "zh" && locale.country == "TW" -> "traditional-chinese"
locale?.language == "zh" && locale.country == "HK" -> "cantonese"
locale?.language == "zh" -> "chinese"
locale?.language == "de" -> "german"
locale?.language == "it" -> "italian"
locale?.language == "ko" -> "korean"
locale?.language == "es" -> "spanish"
locale?.language == "pt" -> "portuguese"
locale?.language == "ru" -> "russian"
locale?.language == "tr" -> "turkish"
locale?.language == "vi" -> "vietnamese"
locale?.language == "ar" -> "arabic"
locale?.language == "id" -> "bahasa"
locale?.language == "th" -> "thai"
locale?.language == "pl" -> "polish"
locale?.language == "sv" -> "swedish"
locale?.language == "nl" -> "flemish-dutch"
locale?.language == "hu" -> "hungarian"
locale?.language == "hi" -> "hindi"
locale?.language == "he" -> "hebrew"
locale?.language == "el" -> "greek"
locale?.language == "fi" -> "finnish"
locale?.language == "fil" -> "filipino"
locale?.language == "da" -> "danish"
locale?.language == "cs" -> "czech"
locale?.language == "hr" -> "croatian"
locale?.language == "bg" -> "bulgarian"
locale?.language == "fa" -> "persian"
locale?.language == "sk" -> "slovak"
locale?.language == "ro" -> "romanian"
locale?.language == "no" -> "norwegian-bokmal"
locale?.language == "ms" -> "malay"
locale?.language == "lt" -> "lithuanian"
else -> null //all
}
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
// Add language path if specified
val langSlug = getLanguageSlug(filter.locale)
if (langSlug != null) {
append("/lang/")
append(langSlug)
}
when {
!filter.query.isNullOrEmpty() -> {
// Search with language: /lang/french/page/2/?s=example
if (page > 1) {
append("/page/")
append(page)
}
append("/?s=")
append(filter.query.urlEncoded())
}
filter.tags.isNotEmpty() -> {
// Genre filtering doesn't work with language, so we ignore language for genre
if (langSlug == null) {
append("/genre/")
append(filter.tags.first().key)
append("/page/")
append(page)
append("/")
} else {
// If both language and genre are selected, just use language
append("/page/")
append(page)
append("/")
}
}
filter.states.isNotEmpty() -> {
// Status filtering doesn't work with language either
if (langSlug == null) {
append("/status/")
append(
when (filter.states.first()) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "ongoing"
},
)
append("/page/")
append(page)
append("/")
} else {
// If both language and status are selected, just use language
append("/page/")
append(page)
append("/")
}
}
else -> {
// Regular browsing with or without language
append("/page/")
append(page)
append("/")
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return parseMangaList(doc)
}
private fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.content-archive article.post:not(.category-video)").mapNotNull { element ->
val titleElement = element.selectFirst("h2.entry-title a") ?: return@mapNotNull null
val thumbnailElement = element.selectFirst("a.entry-image-link img")
Manga(
id = generateUid(titleElement.attr("href")),
title = titleElement.text().replace(titleRegex.toRegex(), "").substringBeforeLast("(").trim(),
altTitles = emptySet(),
url = titleElement.attrAsRelativeUrl("href"),
publicUrl = titleElement.absUrl("href"),
rating = RATING_UNKNOWN,
contentRating = ContentRating.ADULT,
coverUrl = findImageSrc(thumbnailElement),
tags = emptySet(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val title = doc.selectFirst("h1.entry-title")?.text() ?: manga.title
val altTitles = mutableSetOf<String>()
val altTitleElement = doc.selectFirst("p.alt-title-class")
if (altTitleElement != null) {
var nextElement = altTitleElement.nextElementSibling()
while (nextElement != null && nextElement.tagName() == "p" &&
!nextElement.hasClass("info-class") && !nextElement.hasClass("chapter-class")
) {
val altTitle = nextElement.text().trim()
if (altTitle.isNotEmpty()) {
altTitles.add(altTitle)
}
nextElement = nextElement.nextElementSibling()
}
}
var description = ""
val descriptionElement = doc.selectFirst("p.info-class")
if (descriptionElement != null) {
var nextElement = descriptionElement.nextElementSibling()
val descParts = mutableListOf<String>()
while (nextElement != null && nextElement.tagName() == "p" &&
!nextElement.hasClass("chapter-class") && !nextElement.hasClass("alt-title-class")
) {
val text = nextElement.text()
if (text.isNotEmpty()) {
descParts.add(text)
}
nextElement = nextElement.nextElementSibling()
}
description = descParts.joinToString("\n\n")
}
if (description.isEmpty()) {
description = doc.select("div.entry-content p strong")
.joinToString("\n") { it.text() }
.trim()
.ifEmpty { title }
}
val authorFromTitle = title.substringAfter("[").substringBefore("]").trim()
val authorFromTag = doc.select("span.entry-tags a[href*='/tag/']")
.firstOrNull { it.text().contains("(") && it.text().contains(")") }
?.text()?.trim()
val author = authorFromTag ?: authorFromTitle
val genres = mutableSetOf<MangaTag>()
doc.select("span.entry-terms:has(span:contains(Genres)) a").forEach {
genres.add(
MangaTag(
title = it.text(),
key = it.attr("href").substringAfterLast("/genre/").substringBefore("/"),
source = source,
),
)
}
val state = when (doc.select("a[href*=status]").firstOrNull()?.text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
else -> null
}
val chapters = parseChapters(doc)
return manga.copy(
altTitles = altTitles,
description = description,
tags = genres,
state = state,
authors = setOfNotNull(author.takeIf { it.isNotEmpty() && it != title }),
chapters = chapters,
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val images = doc.select("div.entry-content img.img-myreadingmanga, div.entry-content div > img")
.filter { element ->
val src = findImageSrc(element)
src != null && !src.contains("GH-") && !src.contains("nucarnival") &&
!src.contains("/wp-content/uploads/202") // Exclude old uploads that might be ads
}
.mapNotNull { findImageSrc(it) }
.distinct()
return images.mapIndexed { index, url ->
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
private suspend fun fetchTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.select("h4.widget-title.widgettitle:contains(Genres) + .tagcloud a")
.mapToSet { element ->
MangaTag(
title = element.text().substringBefore(" ("),
key = element.attr("href").trimEnd('/').substringAfterLast('/'),
source = source,
)
}
}
private val titleRegex = Pattern.compile("""\[[^]]*]""")
private val imgRegex = Pattern.compile("""\.(jpg|png|jpeg|webp)""")
private fun findImageSrc(element: Element?): String? {
element ?: return null
return when {
element.hasAttr("data-src") && imgRegex.matcher(element.attr("data-src")).find() ->
element.absUrl("data-src")
element.hasAttr("data-cfsrc") && imgRegex.matcher(element.attr("data-cfsrc")).find() ->
element.absUrl("data-cfsrc")
element.hasAttr("src") && imgRegex.matcher(element.attr("src")).find() ->
element.absUrl("src")
element.hasAttr("data-lazy-src") ->
element.absUrl("data-lazy-src")
else -> null
}
}
private fun parseChapters(document: Document): List<MangaChapter> {
val chapters = mutableListOf<MangaChapter>()
val mangaUrl = document.baseUri().removeSuffix("/")
val date = parseDate(document.select("time.entry-time").text())
// Look for chapter information
val chapterClass = document.selectFirst("div.chapter-class")
// Check if there's a chapter title after the chapter-class div
var chapterTitle: String? = null
if (chapterClass != null) {
var nextElement = chapterClass.nextElementSibling()
while (nextElement != null && nextElement.tagName() != "div") {
if (nextElement.tagName() == "p" && nextElement.text().contains("Chapter", ignoreCase = true)) {
chapterTitle = nextElement.text().trim()
break
}
nextElement = nextElement.nextElementSibling()
}
}
// Check for pagination
val paginationInContent =
document.select("div.entry-pagination a.page-numbers, div.chapter-class .entry-pagination a.page-numbers")
.mapNotNull { it.text().toIntOrNull() }
.maxOrNull()
if (paginationInContent != null && paginationInContent > 1) {
// Multi-page manga with chapters
for (i in 1..paginationInContent) {
val title = when {
chapterTitle != null && i == 1 -> chapterTitle
chapterTitle != null -> chapterTitle.replace("1", i.toString())
else -> "Chapter $i"
}
chapters.add(
MangaChapter(
id = generateUid("$mangaUrl/$i"),
title = title,
number = i.toFloat(),
url = if (i == 1) mangaUrl else "$mangaUrl/$i/",
uploadDate = date,
source = source,
scanlator = null,
branch = null,
volume = 0,
),
)
}
} else {
// Single page manga or no pagination found
chapters.add(
MangaChapter(
id = generateUid(mangaUrl),
title = chapterTitle ?: "Complete",
number = 1f,
url = mangaUrl,
uploadDate = date,
source = source,
scanlator = null,
branch = null,
volume = 0,
),
)
}
return chapters
}
private fun parseDate(date: String): Long {
return try {
SimpleDateFormat("MMMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
} catch (_: Exception) {
0L
}
}
}

View File

@@ -8,8 +8,8 @@ import okhttp3.Interceptor
import okhttp3.Response
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.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@@ -17,17 +17,22 @@ import java.util.*
internal abstract class NineMangaParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
defaultDomain: String,
) : PagedMangaParser(context, source, pageSize = 26), Interceptor {
override val configKeyDomain = ConfigKey.Domain(defaultDomain)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
init {
context.cookieJar.insertCookies(domain, "ninemanga_template_desk=yes")
}
override val headers = super.headers.newBuilder()
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.add("Accept-Language", "en-US;q=0.7,en;q=0.3")
.build()
@@ -35,6 +40,22 @@ internal abstract class NineMangaParser(
SortOrder.POPULARITY,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = true,
isSearchWithFiltersSupported = true,
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = getOrCreateTagMap().values.toSet(),
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
),
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = if (request.url.host == domain) {
@@ -45,43 +66,46 @@ 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, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when {
!query.isNullOrEmpty() -> {
append("/search/?name_sel=&wd=")
append(query.urlEncoded())
append("&page=")
if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty() || !filter.query.isNullOrEmpty()) {
append("/search/")
append("?page=")
append(page.toString())
filter.query?.let {
append("&name_sel=contain&wd=")
append(filter.query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
append("/search/?category_id=")
for (tag in tags) {
append(tag.key)
append(',')
append("&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_")
}
} else {
append("/category/index_")
append(page.toString())
}
append(page)
append(".html")
}
val doc = webClient.httpGet(url).parseHtml()
val root = doc.body().selectFirst("ul.direlist") ?: doc.parseFailed("Cannot find root")
val root = doc.body().selectFirstOrThrow("ul.direlist")
val baseHost = root.baseUri().toHttpUrl().host
return root.select("li").map { node ->
val href = node.selectFirst("a")?.absUrl("href") ?: node.parseFailed("Link not found")
val href = node.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val relUrl = href.toRelativeUrl(baseHost)
val dd = node.selectFirst("dd")
Manga(
@@ -89,11 +113,11 @@ internal abstract class NineMangaParser(
url = relUrl,
publicUrl = href,
title = dd?.selectFirst("a.bookname")?.text()?.toCamelCase().orEmpty(),
altTitle = null,
coverUrl = node.selectFirst("img")?.absUrl("src").orEmpty(),
altTitles = emptySet(),
coverUrl = node.selectFirst("img")?.src(),
rating = RATING_UNKNOWN,
author = null,
isNsfw = false,
authors = emptySet(),
contentRating = null,
tags = emptySet(),
state = null,
source = source,
@@ -111,21 +135,24 @@ internal abstract class NineMangaParser(
val tagMap = getOrCreateTagMap()
val selectTag = infoRoot.getElementsByAttributeValue("itemprop", "genre").first()?.select("a")
val tags = selectTag?.mapNotNullToSet { tagMap[it.text()] }
val author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.textOrNull()
return manga.copy(
title = root.selectFirst("h1[itemprop=name]")?.textOrNull()?.removeSuffix("Manga")?.trimEnd()
?: manga.title,
tags = tags.orEmpty(),
author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.text(),
authors = setOfNotNull(author),
state = parseStatus(infoRoot.select("li a.red").text()),
description = infoRoot.getElementsByAttributeValue("itemprop", "description").first()?.html()
?.substringAfter("</b>"),
chapters = root.selectFirst("div.chapterbox")?.select("ul.sub_vol_ul > li")
?.mapChapters(reversed = true) { i, li ->
val a = li.selectFirst("a.chapter_list_a")
val href =
a?.attrAsRelativeUrlOrNull("href")?.replace("%20", " ") ?: li.parseFailed("Link not found")
val a = li.selectFirstOrThrow("a.chapter_list_a")
val href = a.attrAsRelativeUrl("href").replace("%20", " ")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
title = a.textOrNull(),
number = i + 1f,
volume = 0,
url = href,
uploadDate = parseChapterDateByLang(li.selectFirst("span")?.text().orEmpty()),
source = source,
@@ -138,7 +165,7 @@ internal abstract class NineMangaParser(
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.body().getElementById("page")?.select("option")?.map { option ->
return doc.body().requireElementById("page").select("option").map { option ->
val url = option.attr("value")
MangaPage(
id = generateUid(url),
@@ -146,22 +173,18 @@ internal abstract class NineMangaParser(
preview = null,
source = source,
)
} ?: doc.parseFailed("Pages list not found")
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.selectFirst("a.pic_download")?.absUrl("href") ?: doc.parseFailed("Page image not found")
return root.selectFirstOrThrow("a.pic_download").attrAsAbsoluteUrl("href")
}
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet()
}
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()
@@ -171,7 +194,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,
)
@@ -210,7 +233,7 @@ internal abstract class NineMangaParser(
if (dateWords.size == 3) {
if (dateWords[1].contains(",")) {
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).tryParse(date)
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parseSafe(date)
} else {
val timeAgo = Integer.parseInt(dateWords[0])
return Calendar.getInstance().apply {
@@ -246,49 +269,49 @@ internal abstract class NineMangaParser(
@MangaSourceParser("NINEMANGA_EN", "NineManga English", "en")
class English(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_EN,
MangaParserSource.NINEMANGA_EN,
"www.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_ES", "NineManga Español", "es")
class Spanish(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_ES,
MangaParserSource.NINEMANGA_ES,
"es.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_RU", "NineManga Русский", "ru")
class Russian(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_RU,
MangaParserSource.NINEMANGA_RU,
"ru.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_DE", "NineManga Deutsch", "de")
class Deutsch(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_DE,
MangaParserSource.NINEMANGA_DE,
"de.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_BR", "NineManga Brasil", "pt")
class Brazil(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_BR,
MangaParserSource.NINEMANGA_BR,
"br.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_IT", "NineManga Italiano", "it")
class Italiano(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_IT,
MangaParserSource.NINEMANGA_IT,
"it.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_FR", "NineManga Français", "fr")
class Francais(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_FR,
MangaParserSource.NINEMANGA_FR,
"fr.ninemanga.com",
)
}

View File

@@ -0,0 +1,395 @@
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.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.asTypedList
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.suspendlazy.suspendLazy
import java.text.SimpleDateFormat
import java.util.*
@Broken
@MangaSourceParser("NINENINENINEHENTAI", "AnimeH", type = ContentType.HENTAI)
internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.NINENINENINEHENTAI, PAGE_SIZE), Interceptor {
override val configKeyDomain = ConfigKey.Domain("animeh.to")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.NEWEST,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableLocales = 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(initializer = ::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"
}
private suspend fun fetchAvailableTags(): 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, order: SortOrder, filter: MangaListFilter): List<Manga> {
return if (filter.query.isNullOrEmpty()) {
if (filter.tags.isEmpty() && order == SortOrder.POPULARITY) {
getPopularList(page, filter.locale)
} else {
getSearchList(page, null, filter.locale, filter.tags, order)
}
} else {
getSearchList(page, filter.query, null, null, order)
}
}
private suspend fun getPopularList(
page: Int,
locale: Locale?,
): List<Manga> {
val query = """
queryPopularChapters(
size: $PAGE_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: $PAGE_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(),
altTitles = setOf(name),
coverUrl = when {
cover?.startsWith("http") == true -> cover
cover == null -> null
else -> "https://${cdnHost.get()}/$cover"
},
authors = emptySet(),
contentRating = ContentRating.ADULT,
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"),
)
}
val author = tags?.filter { it.type == "artist" }?.joinToString { it.name.toCamelCase() }?.nullIfEmpty()
return manga.copy(
title = name.replace(shortenTitleRegex, "").trim(),
altTitles = setOf(name),
coverUrl = cover.first,
largeCoverUrl = cover.second,
authors = setOfNotNull(author),
contentRating = ContentRating.ADULT,
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),
title = name,
number = 1f,
volume = 0,
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: $PAGE_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").asTypedList<JSONObject>()
val picsS = pages.getJSONArray("picsS").asTypedList<JSONObject>()
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 PAGE_SIZE = 20
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
}

View File

@@ -0,0 +1,299 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.arraySetOf
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
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.getStringOrNull
import java.util.EnumSet
internal abstract class WebtoonsParser(
context: MangaLoaderContext,
source: MangaParserSource,
) : AbstractMangaParser(context, source) {
override val configKeyDomain = ConfigKey.Domain("webtoons.com")
private val mobileApiDomain = "m.webtoons.com"
private val staticDomain = "webtoon-phinf.pstatic.net"
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.RATING,
SortOrder.UPDATED,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override val userAgentKey =
ConfigKey.UserAgent("Mozilla/5.0 (Linux; Android 12; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36")
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = availableTags(),
)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getPageUrl(page: MangaPage): String {
return page.url.toAbsoluteUrl(staticDomain)
}
private val languageCode: String
get() = when (val tag = sourceLocale.toLanguageTag()) {
"in" -> "id"
"zh" -> "zh-hant"
else -> tag
}
private suspend fun fetchEpisodes(titleNo: Long): List<MangaChapter> {
val url = "https://$mobileApiDomain/api/v1/webtoon/$titleNo/episodes?pageSize=99999"
val json = webClient.httpGet(url).parseJson()
val episodeList = json.optJSONObject("result")?.optJSONArray("episodeList")
?: throw ParseException("No episodes found for title $titleNo", url)
return episodeList.mapChapters { _, jo ->
val episodeTitle = jo.getStringOrNull("episodeTitle") ?: ""
val episodeNo = jo.getInt("episodeNo")
val viewerLink = jo.getString("viewerLink")
MangaChapter(
id = generateUid("$titleNo-$episodeNo"),
title = episodeTitle,
number = episodeNo.toFloat(),
volume = 0,
url = viewerLink,
uploadDate = jo.getLong("exposureDateMillis"),
branch = null,
scanlator = null,
source = source,
)
}.sortedBy(MangaChapter::number)
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val titleNo = manga.url.toLong()
val detailsUrl = manga.publicUrl.ifBlank {
"https://$domain/$languageCode/drama/placeholder/list?title_no=$titleNo"
}
val doc = webClient.httpGet(detailsUrl).parseHtml()
val title = doc.select("meta[property='og:title']").attr("content")
.ifEmpty { doc.select("h1.subj, h3.subj").text().ifEmpty { manga.title } }
val description = listOf(
doc.select("meta[property='og:description']").attr("content"),
doc.select("#_asideDetail p.summary").text(),
doc.select(".detail_header .summary").text(),
).firstOrNull { it.isNotBlank() }.orEmpty()
val coverUrl = doc.select("meta[property=\"og:image\"]").attr("content").let { url ->
if (url.isNotBlank()) url.toAbsoluteUrl(staticDomain) else manga.coverUrl
}
val author = listOf(
doc.select("meta[property='com-linewebtoon:webtoon:author']").attr("content"),
doc.select(".detail_header .info .author").firstOrNull()?.text(),
doc.select(".author_area").text(),
).firstOrNull { !it.isNullOrBlank() && it != "null" }
val genreElements = doc.select(".detail_header .info .genre").ifEmpty {
doc.select("h2.genre")
}
val genres = genreElements.map { it.text() }.toSet()
val dayInfo = doc.select("#_asideDetail p.day_info").text().ifEmpty {
doc.select(".day_info").text()
}
val state = when {
dayInfo.contains("UP") || dayInfo.contains("EVERY") || dayInfo.contains("NOUVEAU") -> MangaState.ONGOING
dayInfo.contains("END") || dayInfo.contains("COMPLETED") || dayInfo.contains("TERMINÉ") -> MangaState.FINISHED
else -> null
}
val chapters = async { fetchEpisodes(titleNo) }.await()
Manga(
id = generateUid(titleNo),
title = title,
altTitles = emptySet(),
url = "$titleNo",
publicUrl = detailsUrl,
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = coverUrl,
largeCoverUrl = null,
tags = genres.map { genre -> MangaTag(title = genre, key = genre.lowercase(), source = source) }.toSet(),
authors = setOfNotNull(author.takeIf { it != "null" }),
description = description,
state = state,
chapters = chapters,
source = source,
)
}
private fun getSortOrderParam(order: SortOrder): String {
return when (order) {
SortOrder.POPULARITY -> "MANA"
SortOrder.RATING -> "LIKEIT"
SortOrder.UPDATED -> "UPDATE"
else -> "MANA"
}
}
private fun availableTags() = arraySetOf(
MangaTag("Action", "action", source),
MangaTag("Comedy", "comedy", source),
MangaTag("Drama", "drama", source),
MangaTag("Fantasy", "fantasy", source),
MangaTag("Horror", "horror", source),
MangaTag("Romance", "romance", source),
MangaTag("Sci-Fi", "sf", source),
MangaTag("Slice of Life", "slice_of_life", source),
MangaTag("Sports", "sports", source),
MangaTag("Supernatural", "supernatural", source),
MangaTag("Thriller", "thriller", source),
MangaTag("Historical", "historical", source),
MangaTag("Mystery", "mystery", source),
MangaTag("Superhero", "super_hero", source),
MangaTag("Heartwarming", "heartwarming", source),
MangaTag("Graphic Novel", "graphic_novel", source),
MangaTag("Informative", "tiptoon", source),
)
private val genreUrlMap: Map<String, String> = availableTags().associate {
it.title.lowercase() to it.key
}
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val document = when {
!filter.query.isNullOrEmpty() -> {
val searchUrl = "https://$domain/$languageCode/search?keyword=${filter.query.urlEncoded()}"
webClient.httpGet(searchUrl).parseHtml()
}
filter.tags.isNotEmpty() -> {
val selectedGenre = filter.tags.first()
val genreUrlPath = genreUrlMap[selectedGenre.key] ?: selectedGenre.key
val sortParam = getSortOrderParam(order)
val genreUrl = "https://$domain/$languageCode/genres/$genreUrlPath?sortOrder=$sortParam"
webClient.httpGet(genreUrl).parseHtml()
}
else -> {
val rankingType = when (order) {
SortOrder.POPULARITY -> "popular"
SortOrder.RATING -> "trending"
SortOrder.UPDATED -> "originals"
else -> "popular"
}
val rankingUrl = "https://$domain/$languageCode/ranking/$rankingType"
webClient.httpGet(rankingUrl).parseHtml()
}
}
val selectedGenreForManga = if (filter.tags.isNotEmpty()) filter.tags.first() else null
return document.select(".webtoon_list li a, .card_wrap .card_item a")
.map { element -> createMangaFromElement(element, source, selectedGenreForManga) }
.drop(offset)
.take(20)
}
private fun createMangaFromElement(
element: Element,
source: MangaParserSource,
selectedGenre: MangaTag? = null,
): Manga {
val href = element.absUrl("href")
val titleNo = extractTitleNoFromUrl(href)
val title = element.select(".title, .card_title").text()
val thumbnailUrl = element.select("img").attr("src")
return Manga(
id = generateUid(titleNo),
title = title,
altTitles = emptySet(),
url = titleNo.toString(),
publicUrl = href,
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = thumbnailUrl.toAbsoluteUrl(staticDomain),
largeCoverUrl = null,
tags = selectedGenre?.let { setOf(it) } ?: emptySet(),
authors = emptySet(),
description = null,
state = null,
source = source,
)
}
private fun extractTitleNoFromUrl(url: String): Long {
return Regex("title_no=(\\d+)").find(url)?.groupValues?.get(1)?.toLong()
?: throw ParseException("Could not extract title_no from URL: $url", url)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = try {
val absUrl = chapter.url.toAbsoluteUrl(domain)
webClient.httpGet(absUrl).parseHtml()
} catch (e: Exception) {
throw ParseException("Failed to get pages for chapter: ${chapter.title}", chapter.url, e)
}
fun extractImages(selector: String, attr: String = "data-url"): List<MangaPage> {
return doc.select(selector).mapIndexedNotNull { i, element ->
val url = element.attr(attr).takeIf { it.isNotBlank() }
?: element.attr("src").takeIf { it.contains(staticDomain) }
?: return@mapIndexedNotNull null
MangaPage(
id = generateUid("${chapter.id}-$i"),
url = url,
preview = null,
source = source,
)
}
}
return extractImages("div#_imageList > img")
.ifEmpty { extractImages("canvas[data-url]") }
.ifEmpty { extractImages("img[src*='$staticDomain'], img[data-url*='$staticDomain']") }
.ifEmpty { throw ParseException("No images found in chapter.", chapter.url) }
}
@MangaSourceParser("WEBTOONS_EN", "Webtoons English", "en", type = ContentType.MANGA)
class English(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_EN)
@MangaSourceParser("WEBTOONS_ID", "Webtoons Indonesia", "id", type = ContentType.MANGA)
class Indonesian(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_ID)
@MangaSourceParser("WEBTOONS_ES", "Webtoons Spanish", "es", type = ContentType.MANGA)
class Spanish(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_ES)
@MangaSourceParser("WEBTOONS_FR", "Webtoons French", "fr", type = ContentType.MANGA)
class French(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_FR)
@MangaSourceParser("WEBTOONS_TH", "Webtoons Thai", "th", type = ContentType.MANGA)
class Thai(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_TH)
@MangaSourceParser("WEBTOONS_ZH", "Webtoons Chinese", "zh", type = ContentType.MANGA)
class Chinese(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_ZH)
@MangaSourceParser("WEBTOONS_DE", "Webtoons German", "de", type = ContentType.MANGA)
class German(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_DE)
}

View File

@@ -5,24 +5,25 @@ import kotlinx.coroutines.coroutineScope
import org.json.JSONArray
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
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,
source: MangaParserSource,
domain: String,
pageSize: Int = 24,
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain)
override val isMultipleTagsSupported = false
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
@@ -34,19 +35,27 @@ internal abstract class AnimeBootstrapParser(
protected open val listUrl = "/manga"
protected open val datePattern = "dd MMM. yyyy"
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
isSearchWithFiltersSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(),
availableContentTypes = EnumSet.of(
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
@@ -55,24 +64,37 @@ internal abstract class AnimeBootstrapParser(
append(page.toString())
append("&type=all")
if (!query.isNullOrEmpty()) {
filter.query?.let {
append("&search=")
append(query.urlEncoded())
append(filter.query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("&categorie=")
append(tag?.key.orEmpty())
append(it.key)
}
filter.types.oneOrThrowIfMany()?.let {
append("&type=")
append(
when (it) {
ContentType.MANGA -> "manga"
ContentType.MANHWA -> "manhwa"
ContentType.MANHUA -> "manhua"
else -> "all"
},
)
}
append("&sort=")
when (sortOrder) {
when (order) {
SortOrder.POPULARITY -> append("view")
SortOrder.UPDATED -> append("updated")
SortOrder.ALPHABETICAL -> append("default")
SortOrder.NEWEST -> append("published")
else -> append("updated")
}
}
val doc = webClient.httpGet(url).parseHtml()
@@ -82,24 +104,25 @@ internal abstract class AnimeBootstrapParser(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.product__item__pic").attr("data-setbg").orEmpty(),
coverUrl = div.selectFirstOrThrow("div.product__item__pic")
.attrAsAbsoluteUrlOrNull("data-setbg"),
title = div.selectFirstOrThrow("div.product__item__text").text().orEmpty(),
altTitle = null,
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
authors = emptySet(),
state = null,
source = source,
isNsfw = isNsfwSource,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
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 +138,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 {
@@ -127,7 +147,7 @@ internal abstract class AnimeBootstrapParser(
}
manga.copy(
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
tags = doc.body().select(selectTag).mapToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast('='),
title = a.text().toTitleCase().replace(",", ""),
@@ -148,8 +168,9 @@ internal abstract class AnimeBootstrapParser(
val href = a.attr("href")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
title = a.text(),
number = i + 1f,
volume = 0,
url = href,
uploadDate = 0,
source = source,

View File

@@ -1,95 +1,88 @@
package org.koitharu.kotatsu.parsers.site.animebootstrap.fr
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
import java.util.*
@Broken
@MangaSourceParser("PAPSCAN", "PapScan", "fr")
internal class PapScan(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") {
AnimeBootstrapParser(context, MangaParserSource.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, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/filterList")
append("?page=")
append(page.toString())
when {
!filter.query.isNullOrEmpty() -> {
append("&alpha=")
append(filter.query.urlEncoded())
}
if (!query.isNullOrEmpty()) {
append("&alpha=")
append(query.urlEncoded())
}
else -> {
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 (order) {
SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL_DESC -> append("name&asc=false")
SortOrder.ALPHABETICAL -> append("name&asc=true")
else -> append("updated")
}
}
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.product__item").map { div ->
val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("div.product__item__pic").attr("data-setbg").orEmpty(),
coverUrl = div.selectFirstOrThrow("div.product__item__pic")
.attrAsAbsoluteUrlOrNull("data-setbg"),
title = div.selectFirstOrThrow("div.product__item__text h5").text().orEmpty(),
altTitle = null,
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
authors = emptySet(),
state = null,
source = source,
isNsfw = isNsfwSource,
contentRating = if (isNsfwSource) ContentRating.ADULT else null,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("a.category ").mapNotNullToSet { a ->
return doc.select("a.category ").mapToSet { a ->
val key = a.attr("href").substringAfterLast('=')
val name = a.text()
MangaTag(
@@ -103,19 +96,15 @@ 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 ->
tags = doc.body().select(selectTag).mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
title = a.text().toTitleCase(),
@@ -135,15 +124,15 @@ internal class PapScan(context: MangaLoaderContext) :
val dateText = li.selectFirst("span.date-chapter-title-rtl")?.text()
MangaChapter(
id = generateUid(href),
name = li.selectFirstOrThrow("span em").text(),
number = i + 1,
title = li.selectFirstOrThrow("span em").text(),
number = i + 1f,
volume = 0,
url = href,
uploadDate = dateFormat.tryParse(dateText),
uploadDate = dateFormat.parseSafe(dateText),
source = source,
scanlator = null,
branch = null,
)
}
}
}

View File

@@ -2,9 +2,9 @@ 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.model.MangaParserSource
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, MangaParserSource.KOMIKZOID, "komikzoid.id")

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