diff --git a/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt b/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt
deleted file mode 100644
index 548843850..000000000
--- a/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package eu.kanade.tachiyomi.extension.pt.mizumangas
-
-import eu.kanade.tachiyomi.multisrc.madara.Madara
-import eu.kanade.tachiyomi.network.interceptor.rateLimit
-import okhttp3.OkHttpClient
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.concurrent.TimeUnit
-
-class MizuMangas : Madara(
- "Mizu Mangás",
- "https://mizumangas.com.br",
- "pt-BR",
- SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR")),
-) {
-
- override val client: OkHttpClient = super.client.newBuilder()
- .rateLimit(1, 2, TimeUnit.SECONDS)
- .build()
-}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
index 9b3284bdd..03d84470c 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
@@ -304,7 +304,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("MiniTwo Scan", "https://minitwoscan.com", "pt-BR"),
SingleLang("Mirad Scanlator", "https://miradscanlator.site", "pt-BR", overrideVersionCode = 1),
SingleLang("Mixed Manga", "https://mixedmanga.com", "en", overrideVersionCode = 1),
- SingleLang("Mizu Mangás", "https://mizumangas.com.br", "pt-BR", isNsfw = true, className = "MizuMangas"),
SingleLang("MMScans", "https://mm-scans.org", "en", overrideVersionCode = 5),
SingleLang("Momo no Hana Scan", "https://momonohanascan.com", "pt-BR", className = "MomoNoHanaScan", overrideVersionCode = 1),
SingleLang("MonarcaManga", "https://monarcamanga.com", "es"),
diff --git a/src/pt/mizumangas/AndroidManifest.xml b/src/pt/mizumangas/AndroidManifest.xml
new file mode 100644
index 000000000..389c51256
--- /dev/null
+++ b/src/pt/mizumangas/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/pt/mizumangas/build.gradle b/src/pt/mizumangas/build.gradle
new file mode 100644
index 000000000..6e9b52310
--- /dev/null
+++ b/src/pt/mizumangas/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Mizu Mangás'
+ pkgNameSuffix = 'pt.mizumangas'
+ extClass = '.MizuMangas'
+ extVersionCode = 30
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/mipmap-hdpi/ic_launcher.png
rename to src/pt/mizumangas/res/mipmap-hdpi/ic_launcher.png
diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/mipmap-mdpi/ic_launcher.png
rename to src/pt/mizumangas/res/mipmap-mdpi/ic_launcher.png
diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/mipmap-xhdpi/ic_launcher.png
rename to src/pt/mizumangas/res/mipmap-xhdpi/ic_launcher.png
diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/mipmap-xxhdpi/ic_launcher.png
rename to src/pt/mizumangas/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/pt/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/madara/mizumangas/res/web_hi_res_512.png b/src/pt/mizumangas/res/web_hi_res_512.png
similarity index 100%
rename from multisrc/overrides/madara/mizumangas/res/web_hi_res_512.png
rename to src/pt/mizumangas/res/web_hi_res_512.png
diff --git a/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt
new file mode 100644
index 000000000..497a823f0
--- /dev/null
+++ b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt
@@ -0,0 +1,151 @@
+package eu.kanade.tachiyomi.extension.pt.mizumangas
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
+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.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.util.concurrent.TimeUnit
+
+class MizuMangas : HttpSource() {
+
+ override val name = "Mizu Mangás"
+
+ override val baseUrl = "https://mizumangas.com.br"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ // Migrated from Madara to a custom CMS.
+ override val versionId = 2
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .rateLimitHost(baseUrl.toHttpUrl(), 1, 1, TimeUnit.SECONDS)
+ .rateLimitHost(API_URL.toHttpUrl(), 1, 1, TimeUnit.SECONDS)
+ .rateLimitHost(CDN_URL.toHttpUrl(), 1, 2, TimeUnit.SECONDS)
+ .build()
+
+ private val json: Json by injectLazy()
+
+ private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() }
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Origin", baseUrl)
+ .add("Referer", baseUrl)
+
+ private fun apiHeadersBuilder(): Headers.Builder = headersBuilder()
+ .add("Accept", ACCEPT_JSON)
+
+ /**
+ * The site doesn't have a popular section, so we use latest instead.
+ */
+ override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
+
+ override fun popularMangaParse(response: Response) = latestUpdatesParse(response)
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$API_URL/manga?page=$page&per_page=60", apiHeaders)
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val result = response.parseAs>()
+ val workList = result.data.map(MizuMangasWorkDto::toSManga)
+
+ return MangasPage(workList, result.hasNextPage)
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ // The search with query isn't working in their direct API for some reason,
+ // so we use their site wrapped API instead for now.
+ val apiUrl = "$API_URL/search".toHttpUrl().newBuilder()
+ .addPathSegment(query)
+ .build()
+
+ return GET(apiUrl, apiHeaders)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val result = response.parseAs()
+ val workList = result.mangas.map(MizuMangasWorkDto::toSManga)
+
+ return MangasPage(workList, hasNextPage = false)
+ }
+
+ override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
+
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ val id = manga.url.substringAfter("/manga/")
+
+ return GET("$API_URL/manga/$id", apiHeaders)
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ return response.parseAs().toSManga()
+ }
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val id = manga.url.substringAfter("/manga/")
+
+ return GET("$API_URL/chapter/manga/all/$id", apiHeaders)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return response.parseAs>()
+ .map(MizuMangasChapterDto::toSChapter)
+ }
+
+ override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val id = chapter.url.substringAfter("/reader/")
+
+ return GET("$API_URL/chapter/$id")
+ }
+
+ override fun pageListParse(response: Response): List {
+ val result = response.parseAs()
+ val chapterUrl = "$baseUrl/manga/reader/${result.id}"
+
+ return result.pages.mapIndexed { i, pageDto ->
+ Page(i, chapterUrl, "$CDN_URL/${pageDto.page}")
+ }
+ }
+
+ override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!)
+
+ override fun imageUrlParse(response: Response): String = ""
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .add("Accept", ACCEPT_IMAGE)
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromString(it.body.string())
+ }
+
+ companion object {
+ private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
+ private const val ACCEPT_JSON = "application/json"
+
+ private const val API_URL = "https://api.mizumangas.com.br"
+ const val CDN_URL = "https://cdn.mizumangas.com.br"
+ }
+}
diff --git a/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt
new file mode 100644
index 000000000..b3a41391b
--- /dev/null
+++ b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt
@@ -0,0 +1,85 @@
+package eu.kanade.tachiyomi.extension.pt.mizumangas
+
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+@Serializable
+data class MizuMangasPaginatedContent(
+ @SerialName("current_page") val currentPage: Int,
+ val data: List = emptyList(),
+ @SerialName("last_page") val lastPage: Int,
+) {
+
+ val hasNextPage: Boolean
+ get() = currentPage < lastPage
+}
+
+@Serializable
+data class MizuMangasSearchDto(val mangas: List)
+
+@Serializable
+data class MizuMangasWorkDto(
+ val id: Int,
+ val photo: String? = null,
+ val synopsis: String? = null,
+ val name: String,
+ val status: MizuMangasStatusDto? = null,
+ val categories: List = emptyList(),
+ val people: List = emptyList(),
+) {
+
+ fun toSManga(): SManga = SManga.create().apply {
+ title = name
+ author = people.joinToString { it.name }
+ description = synopsis
+ genre = categories.joinToString { it.name }
+ status = when (this@MizuMangasWorkDto.status?.name) {
+ "Ativo" -> SManga.ONGOING
+ "Completo" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ thumbnail_url = "${MizuMangas.CDN_URL}/$photo"
+ url = "/manga/$id"
+ }
+}
+
+@Serializable
+data class MizuMangasStatusDto(val name: String)
+
+@Serializable
+data class MizuMangasCategoryDto(val name: String)
+
+@Serializable
+data class MizuMangasStaffDto(val name: String)
+
+@Serializable
+data class MizuMangasChapterDto(
+ val id: Int,
+ @SerialName("chapter") val number: String,
+ @SerialName("created_at") val createdAt: String? = null,
+ @SerialName("manga_pages") val pages: List = emptyList(),
+) {
+
+ fun toSChapter(): SChapter = SChapter.create().apply {
+ name = "Capítulo $number"
+ chapter_number = number.toFloatOrNull() ?: -1f
+ date_upload = runCatching { DATE_FORMATTER.parse(createdAt!!)?.time }
+ .getOrNull() ?: 0L
+ url = "/manga/reader/$id"
+ }
+
+ companion object {
+ private val DATE_FORMATTER by lazy {
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US)
+ .apply { timeZone = TimeZone.getTimeZone("UTC") }
+ }
+ }
+}
+
+@Serializable
+data class MizuMangasPageDto(val page: String)