diff --git a/multisrc/overrides/madara/peachscan/src/PeachScan.kt b/multisrc/overrides/madara/peachscan/src/PeachScan.kt
deleted file mode 100644
index 85051217f..000000000
--- a/multisrc/overrides/madara/peachscan/src/PeachScan.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package eu.kanade.tachiyomi.extension.pt.peachscan
-
-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 PeachScan : Madara(
- "Peach Scan",
- "https://www.peachscan.com",
- "pt-BR",
- SimpleDateFormat("dd 'de' MMMMM 'de' yyyy", Locale("pt", "BR")),
-) {
-
- override val client: OkHttpClient = super.client.newBuilder()
- .rateLimit(1, 2, TimeUnit.SECONDS)
- .build()
-
- override val useNewChapterEndpoint = true
-}
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 ca65fbd7d..c7fa928ba 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
@@ -337,7 +337,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("Oh No Manga", "https://ohnomanga.com", "en", isNsfw = true),
SingleLang("Painful Nightz Scan", "https://painfulnightz.com", "en", overrideVersionCode = 1),
SingleLang("Pantheon Scan", "https://pantheon-scan.com", "fr", overrideVersionCode = 1),
- SingleLang("Peach Scan", "https://www.peachscan.com", "pt-BR", isNsfw = true),
SingleLang("Petrotechsociety", "https://www.petrotechsociety.org", "en", isNsfw = true),
SingleLang("Pian Manga", "https://pianmanga.me", "en", isNsfw = true, overrideVersionCode = 1),
SingleLang("Pink Sea Unicorn", "https://psunicorn.com", "pt-BR", isNsfw = true),
diff --git a/src/pt/peachscan/AndroidManifest.xml b/src/pt/peachscan/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/pt/peachscan/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/pt/peachscan/build.gradle b/src/pt/peachscan/build.gradle
new file mode 100644
index 000000000..e43155da3
--- /dev/null
+++ b/src/pt/peachscan/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Peach Scan'
+ pkgNameSuffix = 'pt.peachscan'
+ extClass = '.PeachScan'
+ extVersionCode = 32
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/peachscan/res/mipmap-hdpi/ic_launcher.png b/src/pt/peachscan/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..170eded04
Binary files /dev/null and b/src/pt/peachscan/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/peachscan/res/mipmap-mdpi/ic_launcher.png b/src/pt/peachscan/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..0657447e9
Binary files /dev/null and b/src/pt/peachscan/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/peachscan/res/mipmap-xhdpi/ic_launcher.png b/src/pt/peachscan/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..f6bff07e5
Binary files /dev/null and b/src/pt/peachscan/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/peachscan/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/peachscan/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6fcfafee2
Binary files /dev/null and b/src/pt/peachscan/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/peachscan/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/peachscan/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..732533326
Binary files /dev/null and b/src/pt/peachscan/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/peachscan/res/web_hi_res_512.png b/src/pt/peachscan/res/web_hi_res_512.png
new file mode 100644
index 000000000..cd7133b4b
Binary files /dev/null and b/src/pt/peachscan/res/web_hi_res_512.png differ
diff --git a/src/pt/peachscan/src/eu/kanade/tachiyomi/extension/pt/peachscan/PeachScan.kt b/src/pt/peachscan/src/eu/kanade/tachiyomi/extension/pt/peachscan/PeachScan.kt
new file mode 100644
index 000000000..a5d76c002
--- /dev/null
+++ b/src/pt/peachscan/src/eu/kanade/tachiyomi/extension/pt/peachscan/PeachScan.kt
@@ -0,0 +1,174 @@
+package eu.kanade.tachiyomi.extension.pt.peachscan
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimit
+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.ParsedHttpSource
+import kotlinx.serialization.Serializable
+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 org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+class PeachScan : ParsedHttpSource() {
+
+ override val name = "Peach Scan"
+
+ override val baseUrl = "https://peachscan.com"
+
+ 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()
+ .rateLimit(1, 2, TimeUnit.SECONDS)
+ .build()
+
+ private val json: Json by injectLazy()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder()
+ .add("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
+
+ override fun popularMangaSelector(): String = "section.populares a.populares__links"
+
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.selectFirst("p.nome__obra")!!.text()
+ thumbnail_url = element.selectFirst("img.populares__img")!!.absUrl("src")
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ override fun popularMangaNextPageSelector(): String? = null
+
+ override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
+
+ override fun latestUpdatesSelector() = "section.all__comics div.comic"
+
+ override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.selectFirst("h2.titulo__comic")!!.text()
+ thumbnail_url = element.selectFirst("img.comic__img")!!.absUrl("src")
+ setUrlWithoutDomain(element.selectFirst("a.box-image")!!.attr("href"))
+ }
+
+ override fun latestUpdatesNextPageSelector(): String? = null
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/auto-complete".toHttpUrl().newBuilder()
+ .addQueryParameter("term", query)
+ .toString()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val partialHtml = response.parseAs>()
+ .joinToString("") { it.html }
+
+ val document = Jsoup.parseBodyFragment(partialHtml, baseUrl)
+ val results = document.select(searchMangaSelector())
+ .map(::searchMangaFromElement)
+
+ return MangasPage(results, hasNextPage = false)
+ }
+
+ override fun searchMangaSelector() = "a.autocomplete-link"
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.selectFirst("span.autocomplete-text")!!.text()
+ thumbnail_url = element.selectFirst("img.autocomplete-img")!!.absUrl("src")
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ override fun searchMangaNextPageSelector(): String? = null
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ val descriptionEl = document.selectFirst("section.desc__comics")!!
+
+ title = descriptionEl.selectFirst("h1.desc__titulo__comic")!!.text()
+ author = descriptionEl.selectFirst("div:contains(Autor) + span")!!.text()
+ genre = descriptionEl.select("div:contains(Gênero) + span a")
+ .joinToString { it.text() }
+ status = descriptionEl.selectFirst("div:contains(Status) + span")!!.text().toStatus()
+ description = document.selectFirst("p.sumario__sinopse__texto")!!.text()
+ thumbnail_url = descriptionEl.selectFirst("img.sumario__img")!!.absUrl("src")
+ }
+
+ override fun chapterListSelector() = "ul.capitulos__lista a.link__capitulos"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ name = element.selectFirst("span.numero__capitulo")!!.text()
+ scanlator = name
+ date_upload = element.selectFirst("span.data__lançamento")!!.text().toDate()
+ setUrlWithoutDomain(element.attr("href"))
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val newHeaders = headersBuilder()
+ .set("Referer", baseUrl + chapter.url.substringBeforeLast("/"))
+ .build()
+
+ return GET(baseUrl + chapter.url, newHeaders)
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("div.capitulo img")
+ .mapIndexed { i, element ->
+ Page(i, document.location(), element.absUrl("src"))
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = ""
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", ACCEPT_IMAGE)
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ private fun String.toDate(): Long {
+ return runCatching { DATE_FORMATTER.parse(trim())?.time }
+ .getOrNull() ?: 0L
+ }
+
+ private fun String.toStatus() = when (this) {
+ "Em Lançamento" -> SManga.ONGOING
+ "Completo", "Concluído", "Finalizado" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromString(it.body.string())
+ }
+
+ @Serializable
+ private data class AutoCompleteDto(val html: String)
+
+ companion object {
+ private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8"
+
+ private val DATE_FORMATTER by lazy {
+ SimpleDateFormat("dd 'de' MMMMM 'de' yyyy 'às' HH:mm", Locale("pt", "BR"))
+ }
+ }
+}