From 85d977e407777a846c057870a6304bbd6f480abb Mon Sep 17 00:00:00 2001
From: KenjieDec <65448230+KenjieDec@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:25:35 +0700
Subject: [PATCH] Koharu: Moved to src/all | Spyfakku: Fixed Errors, Added "Per
page" Filter | Pururin: Re-added, Fixed Some Details (#5956)
* Koharu: Moved to src/all | Fixed Spyfakku
- Added 2 language options for Koharu: japanese and english
- Spyfakku: use a cleaner api if available
* Delete src/en/koharu directory
* Fixed #5957
* Added Pururin | Koharu fixed language
- Pururin: Added back, Fixed tags not showing properly
- Koharu: Fixed "Multi" language search not showing anything, added Chinese language as an option
* Fixed Tag Separation in Description
- Fixed: tags were listed with spaces as the separator, instead of commas
* Added Chinese language as an option
- also: applied FourTOne5's suggestion
- I forgor
* Applied suggestion
- Applied FourTOne5's suggestion
* Deeplink support for mirror links
---
src/{en => all}/koharu/AndroidManifest.xml | 14 +-
src/{en => all}/koharu/build.gradle | 4 +-
.../koharu/res/mipmap-hdpi/ic_launcher.png | Bin
.../koharu/res/mipmap-mdpi/ic_launcher.png | Bin
.../koharu/res/mipmap-xhdpi/ic_launcher.png | Bin
.../koharu/res/mipmap-xxhdpi/ic_launcher.png | Bin
.../koharu/res/mipmap-xxxhdpi/ic_launcher.png | Bin
.../tachiyomi/extension/all}/koharu/Koharu.kt | 21 +-
.../extension/all}/koharu/KoharuDto.kt | 2 +-
.../extension/all/koharu/KoharuFactory.kt | 13 +
.../extension/all}/koharu/KoharuFilters.kt | 2 +-
.../all}/koharu/KoharuUrlActivity.kt | 2 +-
src/all/pururin/build.gradle | 8 +
.../pururin/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3244 bytes
.../pururin/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1848 bytes
.../pururin/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4081 bytes
.../pururin/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7099 bytes
.../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9249 bytes
.../extension/all/pururin/Pururin.kt | 271 ++++++++++++++++++
.../extension/all/pururin/PururinFactory.kt | 24 ++
.../extension/all/pururin/PururinFilters.kt | 57 ++++
src/en/spyfakku/build.gradle | 2 +-
.../extension/en/spyfakku/Filters.kt | 10 +
.../extension/en/spyfakku/SpyFakku.kt | 138 +++++----
.../extension/en/spyfakku/SpyFakkuDto.kt | 33 ++-
25 files changed, 518 insertions(+), 83 deletions(-)
rename src/{en => all}/koharu/AndroidManifest.xml (54%)
rename src/{en => all}/koharu/build.gradle (59%)
rename src/{en => all}/koharu/res/mipmap-hdpi/ic_launcher.png (100%)
rename src/{en => all}/koharu/res/mipmap-mdpi/ic_launcher.png (100%)
rename src/{en => all}/koharu/res/mipmap-xhdpi/ic_launcher.png (100%)
rename src/{en => all}/koharu/res/mipmap-xxhdpi/ic_launcher.png (100%)
rename src/{en => all}/koharu/res/mipmap-xxxhdpi/ic_launcher.png (100%)
rename src/{en/koharu/src/eu/kanade/tachiyomi/extension/en => all/koharu/src/eu/kanade/tachiyomi/extension/all}/koharu/Koharu.kt (93%)
rename src/{en/koharu/src/eu/kanade/tachiyomi/extension/en => all/koharu/src/eu/kanade/tachiyomi/extension/all}/koharu/KoharuDto.kt (92%)
create mode 100644 src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt
rename src/{en/koharu/src/eu/kanade/tachiyomi/extension/en => all/koharu/src/eu/kanade/tachiyomi/extension/all}/koharu/KoharuFilters.kt (94%)
rename src/{en/koharu/src/eu/kanade/tachiyomi/extension/en => all/koharu/src/eu/kanade/tachiyomi/extension/all}/koharu/KoharuUrlActivity.kt (95%)
create mode 100644 src/all/pururin/build.gradle
create mode 100644 src/all/pururin/res/mipmap-hdpi/ic_launcher.png
create mode 100644 src/all/pururin/res/mipmap-mdpi/ic_launcher.png
create mode 100644 src/all/pururin/res/mipmap-xhdpi/ic_launcher.png
create mode 100644 src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png
create mode 100644 src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png
create mode 100644 src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt
create mode 100644 src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt
create mode 100644 src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt
diff --git a/src/en/koharu/AndroidManifest.xml b/src/all/koharu/AndroidManifest.xml
similarity index 54%
rename from src/en/koharu/AndroidManifest.xml
rename to src/all/koharu/AndroidManifest.xml
index 5f565204f..e1aa90b82 100644
--- a/src/en/koharu/AndroidManifest.xml
+++ b/src/all/koharu/AndroidManifest.xml
@@ -3,7 +3,7 @@
@@ -13,10 +13,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/src/en/koharu/build.gradle b/src/all/koharu/build.gradle
similarity index 59%
rename from src/en/koharu/build.gradle
rename to src/all/koharu/build.gradle
index 8703ea56e..0bd0a7ad2 100644
--- a/src/en/koharu/build.gradle
+++ b/src/all/koharu/build.gradle
@@ -1,7 +1,7 @@
ext {
extName = 'SchaleNetwork'
- extClass = '.Koharu'
- extVersionCode = 9
+ extClass = '.KoharuFactory'
+ extVersionCode = 10
isNsfw = true
}
diff --git a/src/en/koharu/res/mipmap-hdpi/ic_launcher.png b/src/all/koharu/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from src/en/koharu/res/mipmap-hdpi/ic_launcher.png
rename to src/all/koharu/res/mipmap-hdpi/ic_launcher.png
diff --git a/src/en/koharu/res/mipmap-mdpi/ic_launcher.png b/src/all/koharu/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from src/en/koharu/res/mipmap-mdpi/ic_launcher.png
rename to src/all/koharu/res/mipmap-mdpi/ic_launcher.png
diff --git a/src/en/koharu/res/mipmap-xhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from src/en/koharu/res/mipmap-xhdpi/ic_launcher.png
rename to src/all/koharu/res/mipmap-xhdpi/ic_launcher.png
diff --git a/src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png
rename to src/all/koharu/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/all/koharu/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt
similarity index 93%
rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt
rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt
index 1eb002be0..06cbbe945 100644
--- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt
+++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.en.koharu
+package eu.kanade.tachiyomi.extension.all.koharu
import android.app.Application
import android.content.SharedPreferences
@@ -28,19 +28,21 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
-class Koharu : HttpSource(), ConfigurableSource {
+class Koharu(
+ override val lang: String = "all",
+ private val searchLang: String = "",
+) : HttpSource(), ConfigurableSource {
+
override val name = "SchaleNetwork"
- override val id = 1484902275639232927
-
override val baseUrl = "https://schale.network"
+ override val id = if (lang == "en") 1484902275639232927 else super.id
+
private val apiUrl = baseUrl.replace("://", "://api.")
private val apiBooksUrl = "$apiUrl/books"
- override val lang = "en"
-
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
@@ -112,12 +114,12 @@ class Koharu : HttpSource(), ConfigurableSource {
// Latest
- override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page", headers)
+ override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// Popular
- override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page", headers)
+ override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", headers)
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs()
@@ -143,6 +145,7 @@ class Koharu : HttpSource(), ConfigurableSource {
val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
val terms: MutableList = mutableListOf()
+ if (lang != "all") terms += "language!:\"$searchLang\""
filters.forEach { filter ->
when (filter) {
is SortFilter -> addQueryParameter("sort", filter.getValue())
@@ -158,7 +161,7 @@ class Koharu : HttpSource(), ConfigurableSource {
if (filter.state.isNotEmpty()) {
val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",")
if (tags.isNotBlank()) {
- terms += "${filter.type}!:" + '"' + tags + '"'
+ terms += "${filter.type}!:" + if (filter.type == "pages") tags else '"' + tags + '"'
}
}
}
diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt
similarity index 92%
rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt
rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt
index cf54c190f..fb67dc6e9 100644
--- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt
+++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.en.koharu
+package eu.kanade.tachiyomi.extension.all.koharu
import kotlinx.serialization.Serializable
diff --git a/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt
new file mode 100644
index 000000000..4a3edcb52
--- /dev/null
+++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt
@@ -0,0 +1,13 @@
+package eu.kanade.tachiyomi.extension.all.koharu
+
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+
+class KoharuFactory : SourceFactory {
+ override fun createSources(): List = listOf(
+ Koharu(),
+ Koharu("en", "english"),
+ Koharu("ja", "japanese"),
+ Koharu("zh", "chinese"),
+ )
+}
diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt
similarity index 94%
rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt
rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt
index b871bc400..96bc1d1d0 100644
--- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt
+++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.en.koharu
+package eu.kanade.tachiyomi.extension.all.koharu
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt
similarity index 95%
rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt
rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt
index 56799263d..cb008c2eb 100644
--- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt
+++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.en.koharu
+package eu.kanade.tachiyomi.extension.all.koharu
import android.app.Activity
import android.content.ActivityNotFoundException
diff --git a/src/all/pururin/build.gradle b/src/all/pururin/build.gradle
new file mode 100644
index 000000000..b52cf0832
--- /dev/null
+++ b/src/all/pururin/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'Pururin'
+ extClass = '.PururinFactory'
+ extVersionCode = 10
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/pururin/res/mipmap-hdpi/ic_launcher.png b/src/all/pururin/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..35093ef9b4e06f5556dbc8e24f5d4a95da6f4d4b
GIT binary patch
literal 3244
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
meoKgWhxZzhKOV=Uf$@KNBc*><6>4<=0000m5=
literal 0
HcmV?d00001
diff --git a/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..21db9a1cfe32290b64d1e74db1d95136d25d4b92
GIT binary patch
literal 4081
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
z
zrv++YZmL)uoc-EMAPEj}gXq~mCP0uk8pMV?$bRWIn`n^Dzy2zxpANQ+sJL|qOc+ZB
z&g0DWj4`vBYC+MScy0q8MoU%90??8SfEndSVnV4&4D+wT5#5ywFW5>tfp8X+NxU5|
zO0LqaDfP<0a%NedlSrwBxQf#vzpngIH7%_|O^0}n39(Lp(n!JbZm1BuU_5@0gk!cN
zQOkbmk5lUs{rMis>2Nrxl|4`+2Kqvql#3UbKRjUgrM3|Na#QBm#x+yJep&{TTHsJr
z2{V?y|5R31)&!{)z5BVKwfJn2{T#*j_5D6urUC;@@54f15C{FjIIZC@dz0_!(U`|2
z*W~=V42!fJhJFWJl
zB5e6eA>#*W?kdI)$a-3Pt027#HQelBF-2T+oTgl^13lbi|LnC}Y|`Pnye67g^pa1%
z|Jlx9Mm0;OpyK)P;MkFNN*qbxO|=kBa}htg!Q5#$jVOF$Hybn(u%+2q)-<%R#T;v_
zw#u-W6ObijM&_}8$wfE^^0S!AUJw-H)8Od#)(VZ^>3L|brBks$_3!l`sIgm%HGIAj
zPU2nPRCn>w3@P61Z*<@Hh@%0&k`rzUVB%GQut4#xUf6oyxtUahnZ)l6;MqA#7ha*k
zb8@q1F47U`7^b^fTyrn(aEiPVNkltsM+}u3V6-3-+`zmPPS2||2PXvhW8+*?2rb|s
zxV>(3P;NGvm+RI0zuaRPA(fw<(WqO^Zy&u=UOj9B7@I+?eR%SI*NyG#+Fasc<0{>K
z3e~=vs%v{8zS))9rbExX2x9Rx3T*c9LlS3ls#hthwKcMhfmXa9wZ8(uUODzNJbysmm@KGS~w=g(Vpzp+2V
zjuashmU?N))EMXFaback?u6(0)a&LJ2gR?mIah9*`)JXwk`8ZnduW~R$gB_CXmC3-
z#!CoF8~T82(Tiy|ejRGEc(S)ww{=%j`Z+;7=_A+4mKhdOmZWxWn2)$TeZ~T!#Cf8V
zP=Bp_R|~09dfVPU<|`xV@NS&m%M*=x5z2NAsI=~-hu!Q=SXQ~^rj9At@G2rAK-l*g`@mdd$uHt8TKf;YdA
z>9nh>SFj^l%^yDV_I!hY*+GmhJ^UYhjctzN>|RWkoaDMw;=rjVoW#jbsk#Ue>#Qwm
zsJ1?M`HX+2JcTOcliBP*9Io}Q2hMv1)F&5`sT9B7y=}E5aFx_?s-jE`^yGCw@^kmM+9>bd$6w)e
z74qKanL_VgHnlw9sFyyDtMF_Ut7>KrqnDh9eNY-hJkDeedWk{IiksVCNy99J&Gc|QrGAs1d&2(!GaVD`
literal 0
HcmV?d00001
diff --git a/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..96424681f4c9b62118153bcd2c82767184f6aaf0
GIT binary patch
literal 9249
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
Gx2}u5)1ScJQ+|7oWf~
zL3k3eI(k-t!pS@Utgl*DCy&2x0GwWnsdBs{WbV2*QpU$C(|t~nVsrpbXlA$s;NNC
z8Cg9S9cZl{A#=Q>aE}MrO=;|#Q9cZ
zQ<{_un-tdEDrr}~Ej16Gtg3TAjU~P#CncmCxbB849_joT-VXkiz!=4`N2@U{v=!SP
zg=Jg;-<=Y>*Ia@o&92}7pl?WL_d!ndhmsGi@$7&2tZLBV>^;2FRNPe~`UBaRyrd99
z$aNgM67jKw2H0Q!IV5k3A|Qhv*P=)s(-pGD+rnCFKJu@guP5G@l6Y#Tu#k+_sHQ@E8eG^J!~%aYx4S7JbCOh>db8wxl%p622X)Y*nS4;iDyNqg4lQx^42M2yD0(-4*RVF)$ptfX)?3Y(On@po~5{qN!CiW2cu^D
zin^cIgImZD6Gen4JAGCpL9^eqDwU`f$
zX9>~qYKO08Feb|WiYhCjXhM&NVaguo9kyU0R563A5ZV;YATC%Iq;go&+)WV#j?j5+
zLP6jn>TiiHrO>AO7c#7|+Ld-e1|rqONE?>uS(u=zU|Au9`6+1W_d+!7m$E)-Z`1wa1+~CFj=;W_X|G+q6oZ8YqRjkC_5PJ1bpUz`C=T3zL
z%hDLu*z<#)ZK_}<2+yDYK}h~N2X-Vej4mb9y69o+g$Jc>yWEz;6P_Vkmw$5idmWEz
zc3f||%wWcbrDSh^y?)+&_jL98)Z^f$rq0d>-p%YcJ{C|4?h9#I-N`$T_}E5J$mB_f
z|FqX8Z6IF}awu25L+C$8XIg9O**s4w4qvuEUcP5}j8r%Le7@Ts(%)I~y!f5vy}gI_
z6PXzgt1mC&sy7kKjS3f=b#MQU*tdHZGE9hG7sehk=D!&nnuAv?k!Ea<(aUA;$%3eB
z>hTJeCXaqibb#~@!yWvEX~
zdycktTK?n}^uA}Ag}F=lUZhK&=NCFtOi1d%JWP-mooOr-jhEKc`<(mc=}Al&Q(W95
zp%9{&kP(f-9p}EgxhkCybVi!QHOAR@BAPQYqF@ykM%%3J682J)vMiCzK0$fX-ukyY1X*Jt+kL?<-gX0_
zs)Lx1mfjKsy_HWdC}}Ji;$XKJIaZ%yQq$g1#=b8L)H6Z%CMQwycj-V1IEA7$C`kI=
zbG*oXlYG*yl7s>4qOTbzOC|UVfwdyDD%?yOBU7u|xUw@huf~fc6jAc$3dQf+C(BHn
z`il>LD@ej5^AOwY@sid31EoS710QL0ggE?!x)P_&3+0~O=QDVU(NiQ;CLo-G7)3Zm
z1j{%YS?+Jp=sD;WzT+c${o;tQq`GFm;o$XuJw=lfevY*G=C9au%-Kac`UB`|8{etX
HbdLWYNj5z$
literal 0
HcmV?d00001
diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt
new file mode 100644
index 000000000..8275e55eb
--- /dev/null
+++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt
@@ -0,0 +1,271 @@
+package eu.kanade.tachiyomi.extension.all.pururin
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.FormBody
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.injectLazy
+
+abstract class Pururin(
+ override val lang: String = "all",
+ private val searchLang: Pair? = null,
+ private val langPath: String = "",
+) : ParsedHttpSource() {
+ override val name = "Pururin"
+
+ final override val baseUrl = "https://pururin.me"
+
+ override val supportsLatest = true
+
+ override val client = network.cloudflareClient
+
+ private val json: Json by injectLazy()
+
+ // Popular
+ override fun popularMangaRequest(page: Int): Request {
+ return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
+ }
+
+ override fun popularMangaSelector(): String = "a.card"
+
+ override fun popularMangaFromElement(element: Element): SManga {
+ return SManga.create().apply {
+ title = element.attr("title")
+ setUrlWithoutDomain(element.attr("abs:href"))
+ thumbnail_url = element.select("img").attr("abs:src")
+ }
+ }
+
+ override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
+
+ // Latest
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$baseUrl/browse$langPath?page=$page", headers)
+ }
+
+ override fun latestUpdatesSelector(): String = popularMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
+
+ // Search
+
+ private fun List>.toValue(): String {
+ return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
+ }
+
+ private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair {
+ val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
+ fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
+
+ if (num < 0) return minPages to maxPages
+ return when (query.firstOrNull()) {
+ '<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
+ '>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
+ '=' -> when (query[1]) {
+ '>' -> limitedNum() to maxPages
+ '<' -> 1 to limitedNum(maxPages)
+ else -> limitedNum() to limitedNum()
+ }
+ else -> limitedNum() to limitedNum()
+ }
+ }
+
+ @Serializable
+ class Tag(
+ val id: Int,
+ val name: String,
+ )
+
+ private fun findTagByNameSubstring(tags: List, substring: String): Pair? {
+ val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
+ return tag?.let { Pair(tag.id.toString(), tag.name) }
+ }
+
+ private fun tagSearch(tag: String, type: String): Pair? {
+ val requestBody = FormBody.Builder()
+ .add("text", tag)
+ .build()
+
+ val request = Request.Builder()
+ .url("$baseUrl/api/get/tags/search")
+ .headers(headers)
+ .post(requestBody)
+ .build()
+
+ val response = client.newCall(request).execute()
+ return findTagByNameSubstring(response.parseAs>(), type)
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val includeTags = mutableListOf>()
+ val excludeTags = mutableListOf>()
+ var pagesMin = 1
+ var pagesMax = 9999
+ var sortBy = "newest"
+
+ if (searchLang != null) includeTags.add(searchLang)
+
+ filters.forEach {
+ when (it) {
+ is SelectFilter -> sortBy = it.getValue()
+
+ is TypeFilter -> {
+ val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
+ excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
+ }
+
+ is PageFilter -> {
+ if (it.state.isNotEmpty()) {
+ val (min, max) = parsePageRange(it.state)
+ pagesMin = min
+ pagesMax = max
+ }
+ }
+
+ is TextFilter -> {
+ if (it.state.isNotEmpty()) {
+ it.state.split(",").filter(String::isNotBlank).map { tag ->
+ val trimmed = tag.trim()
+ if (trimmed.startsWith('-')) {
+ tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
+ excludeTags.add(tagInfo)
+ }
+ } else {
+ tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
+ includeTags.add(tagInfo)
+ }
+ }
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+
+ // Searching with just one tag usually gives wrong results
+ if (query.isEmpty()) {
+ when {
+ excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
+ includeTags.size == 1 && excludeTags.isEmpty() -> {
+ val url = baseUrl.toHttpUrl().newBuilder().apply {
+ addPathSegment("browse")
+ addPathSegment("tags")
+ addPathSegment("content")
+ addPathSegment(includeTags[0].first)
+ addQueryParameter("sort", sortBy)
+ addQueryParameter("start_page", pagesMin.toString())
+ addQueryParameter("last_page", pagesMax.toString())
+ if (page > 1) addQueryParameter("page", page.toString())
+ }.build()
+ return GET(url, headers)
+ }
+ }
+ }
+
+ val url = baseUrl.toHttpUrl().newBuilder().apply {
+ addPathSegment("search")
+ addQueryParameter("q", query)
+ addQueryParameter("sort", sortBy)
+ addQueryParameter("start_page", pagesMin.toString())
+ addQueryParameter("last_page", pagesMax.toString())
+ if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
+ if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
+ if (page > 1) addQueryParameter("page", page.toString())
+ }.build()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaSelector(): String = popularMangaSelector()
+
+ override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
+
+ // Details
+
+ override fun mangaDetailsParse(document: Document): SManga {
+ return SManga.create().apply {
+ document.select(".box-gallery").let { e ->
+ initialized = true
+ title = e.select(".title").text()
+ author = e.select("a[href*=/circle/]").eachText().joinToString().ifEmpty { e.select("[itemprop=author]").text() }
+ artist = e.select("[itemprop=author]").eachText().joinToString()
+ genre = e.select("a[href*=/content/]").eachText().joinToString()
+ description = e.select(".box-gallery .table-info tr")
+ .filter { tr ->
+ tr.select("td").let { td ->
+ td.isNotEmpty() &&
+ td.none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
+ }
+ }
+ .joinToString("\n") { tr ->
+ tr.select("td").let { td ->
+ var a = td.select("a").toList()
+ if (a.isEmpty()) a = td.drop(1)
+ td.first()!!.text() + ": " + a.joinToString { it.text() }
+ }
+ }
+ status = SManga.COMPLETED
+ thumbnail_url = e.select("img").attr("abs:src")
+ }
+ }
+ }
+
+ // Chapters
+
+ override fun chapterListSelector(): String = ".table-collection tbody tr a"
+
+ override fun chapterFromElement(element: Element): SChapter {
+ return SChapter.create().apply {
+ name = element.text()
+ setUrlWithoutDomain(element.attr("abs:href"))
+ }
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return response.asJsoup().select(chapterListSelector())
+ .map { chapterFromElement(it) }
+ .reversed()
+ .let { list ->
+ list.ifEmpty {
+ listOf(
+ SChapter.create().apply {
+ setUrlWithoutDomain(response.request.url.toString())
+ name = "Chapter"
+ },
+ )
+ }
+ }
+ }
+
+ // Pages
+
+ override fun pageListParse(document: Document): List {
+ return document.select(".gallery-preview a img")
+ .mapIndexed { i, img ->
+ Page(i, "", (if (img.hasAttr("abs:src")) img.attr("abs:src") else img.attr("abs:data-src")).replace("t.", "."))
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
+
+ private inline fun Response.parseAs(): T {
+ return json.decodeFromString(body.string())
+ }
+ override fun getFilterList() = getFilters()
+}
diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt
new file mode 100644
index 000000000..95117dcd2
--- /dev/null
+++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt
@@ -0,0 +1,24 @@
+package eu.kanade.tachiyomi.extension.all.pururin
+
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+
+class PururinFactory : SourceFactory {
+ override fun createSources(): List = listOf(
+ PururinAll(),
+ PururinEN(),
+ PururinJA(),
+ )
+}
+
+class PururinAll : Pururin()
+class PururinEN : Pururin(
+ "en",
+ Pair("13010", "english"),
+ "/tags/language/13010/english",
+)
+class PururinJA : Pururin(
+ "ja",
+ Pair("13011", "japanese"),
+ "/tags/language/13011/japanese",
+)
diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt
new file mode 100644
index 000000000..40bc4006a
--- /dev/null
+++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt
@@ -0,0 +1,57 @@
+package eu.kanade.tachiyomi.extension.all.pururin
+
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+
+fun getFilters(): FilterList {
+ return FilterList(
+ SelectFilter("Sort by", getSortsList),
+ TypeFilter("Types"),
+ Filter.Separator(),
+ Filter.Header("Separate tags with commas (,)"),
+ Filter.Header("Prepend with dash (-) to exclude"),
+ TextFilter("Tags", "[Content]"),
+ TextFilter("Artists", "[Artist]"),
+ TextFilter("Circles", "[Circle]"),
+ TextFilter("Parodies", "[Parody]"),
+ TextFilter("Languages", "[Language]"),
+ TextFilter("Scanlators", "[Scanlator]"),
+ TextFilter("Conventions", "[Convention]"),
+ TextFilter("Collections", "[Collections]"),
+ TextFilter("Categories", "[Category]"),
+ TextFilter("Uploaders", "[Uploader]"),
+ Filter.Separator(),
+ Filter.Header("Filter by pages, for example: (>20)"),
+ PageFilter("Pages"),
+ )
+}
+internal class TypeFilter(name: String) :
+ Filter.Group(
+ name,
+ listOf(
+ Pair("Artbook", "17783"),
+ Pair("Artist CG", "13004"),
+ Pair("Doujinshi", "13003"),
+ Pair("Game CG", "13008"),
+ Pair("Manga", "13004"),
+ Pair("Webtoon", "27939"),
+ ).map { CheckBoxFilter(it.first, it.second, true) },
+ )
+
+internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
+
+internal open class PageFilter(name: String) : Filter.Text(name)
+
+internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
+
+internal open class SelectFilter(name: String, val vals: List>, state: Int = 0) :
+ Filter.Select(name, vals.map { it.first }.toTypedArray(), state) {
+ fun getValue() = vals[state].second
+}
+private val getSortsList: List> = listOf(
+ Pair("Newest", "newest"),
+ Pair("Most Popular", "most-popular"),
+ Pair("Highest Rated", "highest-rated"),
+ Pair("Most Viewed", "most-viewed"),
+ Pair("Title", "title"),
+)
diff --git a/src/en/spyfakku/build.gradle b/src/en/spyfakku/build.gradle
index 0d857e931..8622e707e 100644
--- a/src/en/spyfakku/build.gradle
+++ b/src/en/spyfakku/build.gradle
@@ -1,7 +1,7 @@
ext {
extName = 'SpyFakku'
extClass = '.SpyFakku'
- extVersionCode = 9
+ extVersionCode = 10
isNsfw = true
}
diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt
index 6f6de5ee9..d67b03351 100644
--- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt
+++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SortFilter("Sort by", Selection(0, false), getSortsList),
+ SelectFilter("Per page", getLimits),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
@@ -25,7 +26,16 @@ internal open class SortFilter(name: String, selection: Selection, private val v
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
+internal open class SelectFilter(name: String, val vals: List, state: Int = 2) :
+ Filter.Select(name, vals.map { it }.toTypedArray(), state)
+private val getLimits = listOf(
+ "6",
+ "12",
+ "24",
+ "36",
+ "48",
+)
private val getSortsList: List> = listOf(
Pair("Title", "title"),
Pair("Relevance", "relevance"),
diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt
index 1f3cb2d05..034c7f355 100644
--- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt
+++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt
@@ -83,6 +83,10 @@ class SpyFakku : HttpSource() {
addQueryParameter("order", if (filter.state!!.ascending) "asc" else "desc")
}
+ is SelectFilter -> {
+ addQueryParameter("limit", filter.vals[filter.state])
+ }
+
is TextFilter -> {
if (filter.state.isNotEmpty()) {
terms += filter.state.split(",").filter { it.isNotBlank() }.map { tag ->
@@ -101,11 +105,6 @@ class SpyFakku : HttpSource() {
return GET(url, headers)
}
- override fun mangaDetailsRequest(manga: SManga): Request {
- manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
- return GET(baseUrl + manga.url.substringBefore("?") + "/__data.json", headers)
- }
-
override fun getFilterList() = getFilters()
// Details
@@ -118,8 +117,8 @@ class SpyFakku : HttpSource() {
}
private fun getAdditionals(data: List): ShortHentai {
- fun Collection.getTags(): List = this.map {
- data[it.jsonPrimitive.int + 2].jsonPrimitive.content
+ fun Collection.getTags(): List = this.map {
+ Name(data[it.jsonPrimitive.int + 2].jsonPrimitive.content, data[it.jsonPrimitive.int + 3].jsonPrimitive.content)
}
val hentaiIndexes = json.decodeFromJsonElement(data[1])
@@ -132,22 +131,14 @@ class SpyFakku : HttpSource() {
val size = data[hentaiIndexes.size].jsonPrimitive.long
val pages = data[hentaiIndexes.pages].jsonPrimitive.int
- val circles = data[hentaiIndexes.circles].jsonArray.emptyToNull()?.getTags()
- val publishers = data[hentaiIndexes.publishers].jsonArray.emptyToNull()?.getTags()
- val magazines = data[hentaiIndexes.magazines].jsonArray.emptyToNull()?.getTags()
- val events = data[hentaiIndexes.events].jsonArray.emptyToNull()?.getTags()
- val parodies = data[hentaiIndexes.parodies].jsonArray.emptyToNull()?.getTags()
+ val tags = data[hentaiIndexes.tags].jsonArray.emptyToNull()?.getTags()
return ShortHentai(
hash = hash,
thumbnail = thumbnail,
description = description,
released_at = released_at,
created_at = created_at,
- publishers = publishers,
- circles = circles,
- magazines = magazines,
- parodies = parodies,
- events = events,
+ tags = tags,
size = size,
pages = pages,
)
@@ -159,62 +150,79 @@ class SpyFakku : HttpSource() {
private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = "/g/$id?$pages&hash=$hash"
- artist = artists?.joinToString()
- genre = tags?.joinToString()
+ author = tags?.filter { it.namespace == "circle" }?.joinToString { it.name }
+ artist = tags?.filter { it.namespace == "artist" }?.joinToString { it.name }
+ genre = tags?.filter { it.namespace == "tag" }?.joinToString { it.name }
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
status = SManga.COMPLETED
}
override fun fetchMangaDetails(manga: SManga): Observable {
- var response: Response = client.newCall(mangaDetailsRequest(manga)).execute()
- var attempts = 0
- while (attempts < 3 && response.code != 200) {
- try {
- response = client.newCall(mangaDetailsRequest(manga)).execute()
- } catch (_: Exception) {
- } finally {
- attempts++
+ val response1: Response = client.newCall(mangaDetailsRequest(manga)).execute()
+ val add: ShortHentai
+
+ if (response1.isSuccessful) {
+ add = response1.parseAs()
+ } else {
+ var response: Response = client.newCall(mangaDetailsRequest(manga)).execute()
+ var attempts = 0
+ while (attempts < 3 && response.code != 200) {
+ try {
+ response = client.newCall(mangaDetailsRequest(manga)).execute()
+ } catch (_: Exception) {
+ } finally {
+ attempts++
+ }
}
+ add = getAdditionals(response.parseAs().nodes.last().data)
}
- val add = getAdditionals(response.parseAs().nodes.last().data)
+
return Observable.just(
manga.apply {
with(add) {
+ val tags = tags?.groupBy { it.namespace }
+
url = "/g/$id?$pages&hash=$hash"
- author = (circles ?: listOf(manga.artist)).joinToString()
+ author = (tags?.get("circle") ?: tags?.get("artist"))?.joinToString { it.name }
+ artist = tags?.get("artist")?.joinToString { it.name }
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
+ genre = tags?.get("tag")?.joinToString { it.name }
this@apply.description = buildString {
description?.let {
append(it, "\n\n")
}
- circles?.emptyToNull()?.joinToString()?.let {
+ tags?.get("circle")?.emptyToNull()?.joinToString { it.name }?.let {
append("Circles: ", it, "\n")
}
- publishers?.emptyToNull()?.joinToString()?.let {
+ tags?.get("publisher")?.emptyToNull()?.joinToString { it.name }?.let {
append("Publishers: ", it, "\n")
}
- magazines?.emptyToNull()?.joinToString()?.let {
+ tags?.get("magazine")?.emptyToNull()?.joinToString { it.name }?.let {
append("Magazines: ", it, "\n")
}
- events?.emptyToNull()?.joinToString()?.let {
+ tags?.get("event")?.emptyToNull()?.joinToString { it.name }?.let {
append("Events: ", it, "\n\n")
}
- parodies?.emptyToNull()?.joinToString()?.let {
+ tags?.get("parody")?.emptyToNull()?.joinToString { it.name }?.let {
append("Parodies: ", it, "\n")
}
append("Pages: ", pages, "\n\n")
try {
- releasedAtFormat.parse(released_at)?.let {
- append("Released: ", dateReformat.format(it.time), "\n")
+ releasedAt?.let {
+ releasedAtFormat.parse(it)?.let {
+ append("Released: ", dateReformat.format(it.time), "\n")
+ }
}
} catch (_: Exception) {
}
try {
- createdAtFormat.parse(created_at)?.let {
- append("Added: ", dateReformat.format(it.time), "\n")
+ createdAt?.let {
+ createdAtFormat.parse(it)?.let {
+ append("Added: ", dateReformat.format(it.time), "\n")
+ }
}
} catch (_: Exception) {
}
@@ -235,22 +243,39 @@ class SpyFakku : HttpSource() {
},
)
}
+
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
+ return GET(baseApiUrl + manga.url.substringBefore("?"), headers)
+ }
+ private fun mangaDetailsRequest2(manga: SManga): Request {
+ manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
+ return GET(baseUrl + manga.url.substringBefore("?") + "/__data.json", headers)
+ }
+
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBefore("?")
// Chapters
override fun fetchChapterList(manga: SManga): Observable> {
- var response: Response = client.newCall(chapterListRequest(manga)).execute()
- var attempts = 0
- while (attempts < 3 && response.code != 200) {
- try {
- response = client.newCall(chapterListRequest(manga)).execute()
- } catch (_: Exception) {
- } finally {
- attempts++
+ val response1: Response = client.newCall(chapterListRequest(manga)).execute()
+ val add: ShortHentai
+
+ if (response1.isSuccessful) {
+ add = response1.parseAs()
+ } else {
+ var response: Response = client.newCall(chapterListRequest2(manga)).execute()
+ var attempts = 0
+ while (attempts < 3 && response.code != 200) {
+ try {
+ response = client.newCall(mangaDetailsRequest(manga)).execute()
+ } catch (_: Exception) {
+ } finally {
+ attempts++
+ }
}
+ add = getAdditionals(response.parseAs().nodes.last().data)
}
- val add = getAdditionals(response.parseAs().nodes.last().data)
return Observable.just(
listOf(
SChapter.create().apply {
@@ -267,13 +292,25 @@ class SpyFakku : HttpSource() {
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBefore("?")
+
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
+ private fun chapterListRequest2(manga: SManga) = mangaDetailsRequest2(manga)
+
override fun chapterListParse(response: Response): List = throw UnsupportedOperationException()
// Pages
override fun fetchPageList(chapter: SChapter): Observable> {
if (!chapter.url.contains("&hash=") && !chapter.url.contains("?")) {
- val response = client.newCall(pageListRequest(chapter)).execute()
+ val response1 = client.newCall(pageListRequest(chapter)).execute()
+ if (response1.isSuccessful) {
+ val hentai = response1.parseAs()
+ return Observable.just(
+ List(hentai.pages) { index ->
+ Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${index + 1}")
+ },
+ )
+ }
+ val response = client.newCall(pageListRequest2(chapter)).execute()
val add = getAdditionals(response.parseAs().nodes.last().data)
return Observable.just(
List(add.pages) { index ->
@@ -292,9 +329,14 @@ class SpyFakku : HttpSource() {
}
override fun pageListRequest(chapter: SChapter): Request {
+ chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }
+ return GET(baseApiUrl + chapter.url.substringBefore("?"), headers)
+ }
+ private fun pageListRequest2(chapter: SChapter): Request {
chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }
return GET(baseUrl + chapter.url.substringBefore("?") + "/__data.json", headers)
}
+
override fun pageListParse(response: Response): List = throw UnsupportedOperationException()
// Others
diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt
index 8cdce1a12..bb2dfb796 100644
--- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt
+++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt
@@ -18,9 +18,7 @@ class Hentai(
val title: String,
val thumbnail: Int,
val pages: Int,
- val artists: List?,
- val circles: List?,
- val tags: List?,
+ val tags: List?,
)
@Serializable
@@ -28,15 +26,24 @@ class ShortHentai(
val hash: String,
val thumbnail: Int,
val description: String?,
- val released_at: String,
- val created_at: String,
- val publishers: List?,
- val circles: List?,
- val magazines: List?,
- val parodies: List?,
- val events: List?,
+ val released_at: String? = null,
+ val created_at: String? = null,
+ var releasedAt: String? = null,
+ var createdAt: String? = null,
+ val tags: List?,
val size: Long,
val pages: Int,
+) {
+ init {
+ releasedAt = released_at ?: releasedAt
+ createdAt = created_at ?: createdAt
+ }
+}
+
+@Serializable
+class Name(
+ val namespace: String,
+ val name: String,
)
@Serializable
@@ -56,11 +63,7 @@ class HentaiIndexes(
val description: Int,
val released_at: Int,
val created_at: Int,
- val publishers: Int,
- val circles: Int,
- val magazines: Int,
- val parodies: Int,
- val events: Int,
+ val tags: Int,
val size: Int,
val pages: Int,
)