diff --git a/.run/KemonoGenerator.run.xml b/.run/KemonoGenerator.run.xml
new file mode 100644
index 000000000..2ac0c58da
--- /dev/null
+++ b/.run/KemonoGenerator.run.xml
@@ -0,0 +1,17 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="KemonoGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
+    <module name="tachiyomi-extensions.multisrc" />
+    <option name="VM_PARAMETERS" value="" />
+    <option name="PROGRAM_PARAMETERS" value="" />
+    <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+    <option name="ALTERNATIVE_JRE_PATH" />
+    <option name="PASS_PARENT_ENVS" value="true" />
+    <option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.kemono.KemonoGenerator" />
+    <option name="WORKING_DIRECTORY" value="" />
+    <method v="2">
+      <option name="Make" enabled="true" />
+      <option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
+      <option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/multisrc/overrides/kemono/coomer/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/kemono/coomer/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..fdf273a9b
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/coomer/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/kemono/coomer/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..bf37a1b7a
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/coomer/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/kemono/coomer/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..f9b11f5f0
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/coomer/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/kemono/coomer/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..04edc56b2
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/coomer/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/kemono/coomer/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..61b49c814
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/coomer/res/web_hi_res_512.png b/multisrc/overrides/kemono/coomer/res/web_hi_res_512.png
new file mode 100644
index 000000000..0f401f9eb
Binary files /dev/null and b/multisrc/overrides/kemono/coomer/res/web_hi_res_512.png differ
diff --git a/multisrc/overrides/kemono/default/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/kemono/default/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..404a9888f
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/default/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/kemono/default/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..b7e10b9f9
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/default/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/kemono/default/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..fabf9b671
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/default/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/kemono/default/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..aeff4ae0e
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/default/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/kemono/default/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1597c0783
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/kemono/default/res/web_hi_res_512.png b/multisrc/overrides/kemono/default/res/web_hi_res_512.png
new file mode 100644
index 000000000..e5cbe8a72
Binary files /dev/null and b/multisrc/overrides/kemono/default/res/web_hi_res_512.png differ
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt
new file mode 100644
index 000000000..5b03d40b7
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/Kemono.kt
@@ -0,0 +1,155 @@
+package eu.kanade.tachiyomi.multisrc.kemono
+
+import android.app.Application
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimit
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Element
+import org.jsoup.select.Evaluator
+import rx.Observable
+import rx.Single
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+import java.util.TimeZone
+
+open class Kemono(
+    override val name: String,
+    override val baseUrl: String,
+    override val lang: String = "all",
+) : HttpSource(), ConfigurableSource {
+    override val supportsLatest = true
+
+    override val client = network.client.newBuilder().rateLimit(2).build()
+
+    private val json: Json by injectLazy()
+
+    private val preferences by lazy {
+        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
+    }
+
+    override fun popularMangaRequest(page: Int): Request =
+        GET("$baseUrl/artists?o=${PAGE_SIZE * (page - 1)}", headers)
+
+    override fun popularMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+        val cardList = document.selectFirst(Evaluator.Class("card-list"))
+        val creators = cardList.select(Evaluator.Tag("article")).map {
+            val children = it.children()
+            val avatar = children[0].selectFirst(Evaluator.Tag("img")).attr("src")
+            val link = children[1].child(0)
+            val service = children[2].ownText()
+            SManga.create().apply {
+                url = link.attr("href")
+                title = link.ownText()
+                author = service
+                thumbnail_url = baseUrl + avatar
+                description = PROMPT
+                initialized = true
+            }
+        }.filterUnsupported()
+        return MangasPage(creators, document.hasNextPage())
+    }
+
+    override fun latestUpdatesRequest(page: Int): Request =
+        GET("$baseUrl/artists/updated?o=${PAGE_SIZE * (page - 1)}", headers)
+
+    override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
+
+    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Single.create<MangasPage> { subscriber ->
+        val baseUrl = this.baseUrl
+        val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute()
+        val result = response.parseAs<List<KemonoCreatorDto>>()
+            .filter { it.name.contains(query, ignoreCase = true) }
+            .sortedByDescending { it.updatedDate }
+            .map { it.toSManga(baseUrl) }
+            .filterUnsupported()
+        subscriber.onSuccess(MangasPage(result, false))
+    }.toObservable()
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used.")
+    override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not used.")
+
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.just(manga)
+
+    override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Not used.")
+
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
+        KemonoPostDto.dateFormat.timeZone = when (manga.author) {
+            "Pixiv Fanbox", "Fantia" -> TimeZone.getTimeZone("GMT+09:00")
+            else -> TimeZone.getTimeZone("GMT")
+        }
+        val maxPosts = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
+            .toInt().coerceAtMost(POST_PAGES_MAX) * POST_PAGE_SIZE
+        var offset = 0
+        var hasNextPage = true
+        val result = ArrayList<SChapter>()
+        while (offset < maxPosts && hasNextPage) {
+            val request = GET("$baseUrl/api${manga.url}?limit=$POST_PAGE_SIZE&o=$offset", headers)
+            val page: List<KemonoPostDto> = client.newCall(request).execute().parseAs()
+            page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
+            offset += POST_PAGE_SIZE
+            hasNextPage = page.size == POST_PAGE_SIZE
+        }
+        it.onSuccess(result)
+    }.toObservable()
+
+    override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.")
+
+    override fun pageListRequest(chapter: SChapter): Request =
+        GET("$baseUrl/api${chapter.url}", headers)
+
+    override fun pageListParse(response: Response): List<Page> {
+        val post: List<KemonoPostDto> = response.parseAs()
+        return post[0].images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
+    }
+
+    override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
+
+    private inline fun <reified T> Response.parseAs(): T = use {
+        json.decodeFromStream(it.body!!.byteStream())
+    }
+
+    override fun setupPreferenceScreen(screen: PreferenceScreen) {
+        ListPreference(screen.context).apply {
+            key = POST_PAGES_PREF
+            title = "Maximum posts to load"
+            summary = "Loading more posts costs more time and network traffic.\nCurrently: %s"
+            entryValues = (1..POST_PAGES_MAX).map { it.toString() }.toTypedArray()
+            entries = (1..POST_PAGES_MAX).map {
+                if (it == 1) "1 page ($POST_PAGE_SIZE posts)" else "$it pages (${it * POST_PAGE_SIZE} posts)"
+            }.toTypedArray()
+            setDefaultValue(POST_PAGES_DEFAULT)
+        }.let { screen.addPreference(it) }
+    }
+
+    companion object {
+        private const val PAGE_SIZE = 25
+        const val PROMPT = "You can change how many posts to load in the extension preferences."
+
+        private const val POST_PAGE_SIZE = 50
+        private const val POST_PAGES_PREF = "POST_PAGES"
+        private const val POST_PAGES_DEFAULT = "1"
+        private const val POST_PAGES_MAX = 50
+
+        private fun Element.hasNextPage(): Boolean {
+            val pagination = selectFirst(Evaluator.Class("paginator"))
+            return pagination.selectFirst("a[title=Next page]") != null
+        }
+
+        private fun List<SManga>.filterUnsupported() = filterNot { it.author == "Discord" }
+    }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt
new file mode 100644
index 000000000..9f50f81c7
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoDto.kt
@@ -0,0 +1,82 @@
+package eu.kanade.tachiyomi.multisrc.kemono
+
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.Serializable
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@Serializable
+class KemonoCreatorDto(
+    private val id: String,
+    val name: String,
+    private val service: String,
+    private val updated: String,
+) {
+    val updatedDate get() = dateFormat.parse(updated)?.time ?: 0
+
+    fun toSManga(baseUrl: String) = SManga.create().apply {
+        url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway
+        title = name
+        author = service.serviceName()
+        thumbnail_url = "$baseUrl/icons/$service/$id"
+        description = Kemono.PROMPT
+        initialized = true
+    }
+
+    companion object {
+        private val dateFormat by lazy { getApiDateFormat() }
+
+        fun String.serviceName() = when (this) {
+            "fanbox" -> "Pixiv Fanbox"
+            "subscribestar" -> "SubscribeStar"
+            "dlsite" -> "DLsite"
+            "onlyfans" -> "OnlyFans"
+            else -> replaceFirstChar { it.uppercase() }
+        }
+    }
+}
+
+@Serializable
+class KemonoPostDto(
+    private val id: String,
+    private val service: String,
+    private val user: String,
+    private val title: String,
+    private val added: String,
+    private val published: String?,
+    private val edited: String?,
+    private val file: KemonoFileDto,
+    private val attachments: List<KemonoAttachmentDto>,
+) {
+    val images: List<String>
+        get() = buildList(attachments.size + 1) {
+            file.path?.let { add(it) }
+            attachments.mapTo(this) { it.path }
+        }.filter {
+            when (it.substringAfterLast('.').lowercase()) {
+                "png", "jpg", "gif", "jpeg", "webp" -> true
+                else -> false
+            }
+        }.distinct()
+
+    fun toSChapter() = SChapter.create().apply {
+        url = "/$service/user/$user/post/$id"
+        name = title
+        date_upload = dateFormat.parse(edited ?: published ?: added)?.time ?: 0
+        chapter_number = -2f
+    }
+
+    companion object {
+        val dateFormat by lazy { getApiDateFormat() }
+    }
+}
+
+@Serializable
+class KemonoFileDto(val path: String? = null)
+
+@Serializable
+class KemonoAttachmentDto(val path: String)
+
+private fun getApiDateFormat() =
+    SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt
new file mode 100644
index 000000000..977e44944
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/kemono/KemonoGenerator.kt
@@ -0,0 +1,21 @@
+package eu.kanade.tachiyomi.multisrc.kemono
+
+import generator.ThemeSourceData.SingleLang
+import generator.ThemeSourceGenerator
+
+class KemonoGenerator : ThemeSourceGenerator {
+    override val themeClass = "Kemono"
+    override val themePkg = "kemono"
+    override val baseVersionCode = 1
+    override val sources = listOf(
+        SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true, className = "KemonoParty", pkgName = "kemono"),
+        SingleLang("Coomer", "https://coomer.party", "all", isNsfw = true)
+    )
+
+    companion object {
+        @JvmStatic
+        fun main(args: Array<String>) {
+            KemonoGenerator().createAll()
+        }
+    }
+}