diff --git a/src/ja/rawz/AndroidManifest.xml b/src/ja/rawz/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/ja/rawz/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/ja/rawz/build.gradle b/src/ja/rawz/build.gradle
new file mode 100644
index 000000000..6d6d84359
--- /dev/null
+++ b/src/ja/rawz/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'RawZ'
+ extClass = '.RawZ'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZ.kt b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZ.kt
new file mode 100644
index 000000000..5cc56a260
--- /dev/null
+++ b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZ.kt
@@ -0,0 +1,180 @@
+package eu.kanade.tachiyomi.extension.ja.rawz
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Filter
+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 kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+import kotlin.UnsupportedOperationException
+
+class RawZ : HttpSource() {
+
+ override val name = "RawZ"
+
+ override val baseUrl = "https://stmanga.com"
+
+ private val apiUrl = "https://api.rawz.org/api"
+
+ override val lang = "ja"
+
+ override val supportsLatest = true
+
+ private val json by injectLazy()
+
+ override val client = network.cloudflareClient
+
+ override fun headersBuilder() = super.headersBuilder()
+ .set("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
+ override fun popularMangaParse(response: Response) = searchMangaParse(response)
+
+ override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
+ override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$apiUrl/manga".toHttpUrl().newBuilder().apply {
+ addQueryParameter("name", query.trim())
+ filters.filterIsInstance().forEach {
+ it.addQueryParameter(this)
+ }
+ addQueryParameter("page", page.toString())
+ addQueryParameter("limit", LIMIT.toString())
+ }.build()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ fetchGenres()
+
+ val result = response.parseAs>>()
+
+ val entries = result.data.map { it.toSManga() }
+ val hasNextPage = result.data.size == LIMIT
+
+ return MangasPage(entries, hasNextPage)
+ }
+
+ private var genreCache: List> = emptyList()
+ private var genreFetchAttempts = 0
+ private var genreFetchFailed = false
+
+ private fun fetchGenres() {
+ if ((genreCache.isEmpty() || genreFetchFailed) && genreFetchAttempts < 3) {
+ val genres = runCatching {
+ client.newCall(
+ GET("$apiUrl/taxonomy-browse?type=genres&limit=100&page=1", headers),
+ )
+ .execute()
+ .parseAs>()
+ .data.genres.map {
+ Pair(it.name, it.id.toString())
+ }
+ }
+
+ genreCache = genres.getOrNull().orEmpty()
+ genreFetchFailed = genres.isFailure
+ genreFetchAttempts++
+ }
+ }
+
+ override fun getFilterList(): FilterList {
+ val filters = mutableListOf>(
+ SortFilter(),
+ TypeFilter(),
+ StatusFilter(),
+ ChapterNumFilter(),
+ )
+
+ filters += if (genreCache.isEmpty()) {
+ listOf(
+ Filter.Separator(),
+ Filter.Header("Press Reset to attempt to display genre"),
+ )
+ } else {
+ listOf(
+ GenreFilter(genreCache),
+ )
+ }
+
+ return FilterList(filters)
+ }
+
+ override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
+
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ return GET("$apiUrl/manga/${manga.url.substringAfter(".")}")
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ return response.parseAs>().data.toSManga()
+ }
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val slug = manga.url
+ .substringAfter("/manga/")
+ .substringBefore(".")
+
+ val id = manga.url.substringAfterLast(".")
+
+ return GET("$apiUrl/manga/$id/childs#$slug", headers)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val result = response.parseAs>>()
+ val mangaSlug = response.request.url.fragment!!
+
+ return result.data.map {
+ SChapter.create().apply {
+ url = "/read/$mangaSlug.${it.id}/${it.slug}"
+ name = it.name
+ date_upload = runCatching {
+ dateFormat.parse(it.createdAt!!)!!.time
+ }.getOrDefault(0L)
+ }
+ }
+ }
+
+ override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val id = chapter.url
+ .substringBeforeLast("/")
+ .substringAfterLast(".")
+
+ return GET("$apiUrl/child-detail/$id", headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val result = response.parseAs>()
+
+ return result.data.images.mapIndexed { idx, img ->
+ Page(idx, "", img.url)
+ }
+ }
+
+ override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
+
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromString(body.string())
+ }
+
+ companion object {
+ private const val LIMIT = 30
+ private val dateFormat by lazy {
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
+ }
+ }
+}
diff --git a/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZDto.kt b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZDto.kt
new file mode 100644
index 000000000..1526cbbd0
--- /dev/null
+++ b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZDto.kt
@@ -0,0 +1,97 @@
+package eu.kanade.tachiyomi.extension.ja.rawz
+
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonTransformingSerializer
+import java.util.Locale
+
+@Serializable
+data class Data(
+ val data: T,
+)
+
+@Serializable
+data class Manga(
+ val id: Int,
+ val name: String,
+ val slug: String,
+ val description: String? = null,
+ val status: String? = null,
+ val type: String? = null,
+ val image: String? = null,
+ @Serializable(with = EmptyArrayOrTaxonomySerializer::class)
+ val taxonomy: Taxonomy,
+) {
+ fun toSManga() = SManga.create().apply {
+ url = "/manga/$slug.$id"
+ thumbnail_url = image
+ title = name
+ description = this@Manga.description
+ genre = (
+ taxonomy.genres.map {
+ it.name
+ }.let {
+ type?.run {
+ it.plus(
+ this.replaceFirstChar {
+ if (it.isLowerCase()) {
+ it.titlecase(Locale.getDefault())
+ } else {
+ it.toString()
+ }
+ },
+ )
+ }
+ }
+ )?.joinToString()
+ status = when (this@Manga.status) {
+ "ongoing" -> SManga.ONGOING
+ "completed" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ initialized = true
+ }
+}
+
+@Serializable
+data class Taxonomy(
+ val genres: List,
+)
+
+object EmptyArrayOrTaxonomySerializer : JsonTransformingSerializer(Taxonomy.serializer()) {
+ override fun transformDeserialize(element: JsonElement): JsonElement {
+ return if (element is JsonArray) {
+ JsonObject(mapOf(Pair("genres", JsonArray(emptyList()))))
+ } else {
+ element
+ }
+ }
+}
+
+@Serializable
+data class Genre(
+ val id: Int,
+ val name: String,
+)
+
+@Serializable
+data class Chapter(
+ val id: Int,
+ val name: String,
+ val slug: String,
+ @SerialName("created_at") val createdAt: String? = null,
+)
+
+@Serializable
+data class Pages(
+ val images: List,
+)
+
+@Serializable
+data class Url(
+ val url: String,
+)
diff --git a/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZFilters.kt b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZFilters.kt
new file mode 100644
index 000000000..b90692bdc
--- /dev/null
+++ b/src/ja/rawz/src/eu/kanade/tachiyomi/extension/ja/rawz/RawZFilters.kt
@@ -0,0 +1,101 @@
+package eu.kanade.tachiyomi.extension.ja.rawz
+
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import okhttp3.HttpUrl
+
+interface UriPartFilter {
+ fun addQueryParameter(url: HttpUrl.Builder)
+}
+
+class CheckBoxFilter(
+ name: String,
+ val value: String,
+) : Filter.CheckBox(name)
+
+abstract class CheckBoxFilterGroup(
+ name: String,
+ genres: List>,
+) : UriPartFilter, Filter.Group(
+ name,
+ genres.map { CheckBoxFilter(it.first, it.second) },
+) {
+ abstract val queryParameter: String
+ override fun addQueryParameter(url: HttpUrl.Builder) {
+ state.filter { it.state }.forEach {
+ url.addQueryParameter(queryParameter, it.value)
+ }
+ }
+}
+
+abstract class SelectFilter(
+ name: String,
+ private val options: List>,
+ defaultValue: String? = null,
+) : UriPartFilter, Filter.Select(
+ name,
+ options.map { it.first }.toTypedArray(),
+ options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
+) {
+ abstract val queryParameter: String
+ override fun addQueryParameter(url: HttpUrl.Builder) {
+ url.addQueryParameter(queryParameter, options[state].second)
+ }
+}
+
+class TypeFilter : CheckBoxFilterGroup("タイプ", types) {
+ override val queryParameter = "type[]"
+ companion object {
+ private val types = listOf(
+ Pair("Manga", "manga"),
+ Pair("Manhua", "manhua"),
+ Pair("Manhwa", "manhwa"),
+ Pair("Oneshot", "oneshot"),
+ Pair("Doujinshi", "doujinshi"),
+ )
+ }
+}
+
+class GenreFilter(genres: List>) : CheckBoxFilterGroup("ジャンル", genres) {
+ override val queryParameter = "taxonomy[]"
+}
+
+class StatusFilter : CheckBoxFilterGroup("ステータス", status) {
+ override val queryParameter = "status[]"
+ companion object {
+ private val status = listOf(
+ Pair("Ongoing", "ongoing"),
+ Pair("Completed", "completed"),
+ )
+ }
+}
+
+class ChapterNumFilter : SelectFilter("最小章", minChapNum) {
+ override val queryParameter = "minchap"
+ companion object {
+ private val minChapNum = listOf(
+ Pair(">= 1 chapters", "1"),
+ Pair(">= 3 chapters", "3"),
+ Pair(">= 5 chapters", "5"),
+ Pair(">= 10 chapters", "10"),
+ Pair(">= 20 chapters", "20"),
+ Pair(">= 30 chapters", "30"),
+ Pair(">= 50 chapters", "50"),
+ )
+ }
+}
+
+class SortFilter(default: String? = null) : SelectFilter("並び替え", sorts, default) {
+ override val queryParameter = "order_by"
+ companion object {
+ private val sorts = listOf(
+ Pair("Recently updated", "updated_at"),
+ Pair("Recently added", "created_at"),
+ Pair("Trending", "views"),
+ Pair("Name A-Z", "name"),
+ )
+
+ val POPULAR = FilterList(SortFilter("views"))
+ val LATEST = FilterList(SortFilter("updated_at"))
+ }
+}