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 @@
 
     <application>
         <activity
-            android:name=".en.koharu.KoharuUrlActivity"
+            android:name=".all.koharu.KoharuUrlActivity"
             android:excludeFromRecents="true"
             android:exported="true"
             android:theme="@android:style/Theme.NoDisplay">
@@ -13,10 +13,14 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
 
-                <data
-                    android:host="koharu.to"
-                    android:pathPattern="/g/..*/..*"
-                    android:scheme="https" />
+                <data android:scheme="https" android:pathPattern="/g/..*/..*"/>
+                <data android:host="koharu.to" />
+                <data android:host="schale.network" />
+                <data android:host="gehenna.jp" />
+                <data android:host="niyaniya.moe" />
+                <data android:host="seia.to" />
+                <data android:host="shupogaki.moe" />
+                <data android:host="hoshino.one" />
             </intent-filter>
         </activity>
     </application>
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<Books>()
 
@@ -143,6 +145,7 @@ class Koharu : HttpSource(), ConfigurableSource {
         val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
             val terms: MutableList<String> = 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<Source> = 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 000000000..35093ef9b
Binary files /dev/null and b/src/all/pururin/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/pururin/res/mipmap-mdpi/ic_launcher.png b/src/all/pururin/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..31974cf68
Binary files /dev/null and b/src/all/pururin/res/mipmap-mdpi/ic_launcher.png differ
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 000000000..21db9a1cf
Binary files /dev/null and b/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..cbd8ea8d0
Binary files /dev/null and b/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png differ
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 000000000..96424681f
Binary files /dev/null and b/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png differ
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<String, String>? = 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<Pair<String, String>>.toValue(): String {
+        return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
+    }
+
+    private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
+        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<Tag>, substring: String): Pair<String, String>? {
+        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<String, String>? {
+        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<List<Tag>>(), type)
+    }
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val includeTags = mutableListOf<Pair<String, String>>()
+        val excludeTags = mutableListOf<Pair<String, String>>()
+        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<SChapter> {
+        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<Page> {
+        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 <reified T> 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<Source> = 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<CheckBoxFilter>(
+        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<Pair<String, String>>, state: Int = 0) :
+    Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
+    fun getValue() = vals[state].second
+}
+private val getSortsList: List<Pair<String, String>> = 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<String>, state: Int = 2) :
+    Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
 
+private val getLimits = listOf(
+    "6",
+    "12",
+    "24",
+    "36",
+    "48",
+)
 private val getSortsList: List<Pair<String, String>> = 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<JsonElement>): ShortHentai {
-        fun Collection<JsonElement>.getTags(): List<String> = this.map {
-            data[it.jsonPrimitive.int + 2].jsonPrimitive.content
+        fun Collection<JsonElement>.getTags(): List<Name> = this.map {
+            Name(data[it.jsonPrimitive.int + 2].jsonPrimitive.content, data[it.jsonPrimitive.int + 3].jsonPrimitive.content)
         }
         val hentaiIndexes = json.decodeFromJsonElement<HentaiIndexes>(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<SManga> {
-        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<ShortHentai>()
+        } 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>().nodes.last().data)
         }
-        val add = getAdditionals(response.parseAs<Nodes>().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<List<SChapter>> {
-        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<ShortHentai>()
+        } 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>().nodes.last().data)
         }
-        val add = getAdditionals(response.parseAs<Nodes>().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<SChapter> = throw UnsupportedOperationException()
 
     // Pages
     override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
         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<Hentai>()
+                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>().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<Page> = 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<String>?,
-    val circles: List<String>?,
-    val tags: List<String>?,
+    val tags: List<Name>?,
 )
 
 @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<String>?,
-    val circles: List<String>?,
-    val magazines: List<String>?,
-    val parodies: List<String>?,
-    val events: List<String>?,
+    val released_at: String? = null,
+    val created_at: String? = null,
+    var releasedAt: String? = null,
+    var createdAt: String? = null,
+    val tags: List<Name>?,
     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,
 )