diff --git a/src/all/paprika/build.gradle b/src/all/paprika/build.gradle
new file mode 100644
index 000000000..f3ebc1ee7
--- /dev/null
+++ b/src/all/paprika/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ appName = 'Tachiyomi: Paprika'
+ pkgNameSuffix = 'all.paprika'
+ extClass = '.PaprikaFactory'
+ extVersionCode = 1
+ libVersion = '1.2'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/paprika/res/mipmap-hdpi/ic_launcher.png b/src/all/paprika/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..329d1eeb3
Binary files /dev/null and b/src/all/paprika/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/paprika/res/mipmap-mdpi/ic_launcher.png b/src/all/paprika/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..299f14b23
Binary files /dev/null and b/src/all/paprika/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/paprika/res/mipmap-xhdpi/ic_launcher.png b/src/all/paprika/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..a3502c2c2
Binary files /dev/null and b/src/all/paprika/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/paprika/res/mipmap-xxhdpi/ic_launcher.png b/src/all/paprika/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..a9cd85b35
Binary files /dev/null and b/src/all/paprika/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/paprika/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/paprika/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6c64322d2
Binary files /dev/null and b/src/all/paprika/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/paprika/res/web_hi_res_512.png b/src/all/paprika/res/web_hi_res_512.png
new file mode 100644
index 000000000..0f5836de3
Binary files /dev/null and b/src/all/paprika/res/web_hi_res_512.png differ
diff --git a/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/Paprika.kt b/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/Paprika.kt
new file mode 100644
index 000000000..7ee28a371
--- /dev/null
+++ b/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/Paprika.kt
@@ -0,0 +1,299 @@
+package eu.kanade.tachiyomi.extension.all.paprika
+
+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.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+abstract class Paprika(
+ override val name: String,
+ override val baseUrl: String,
+ override val lang: String
+) : ParsedHttpSource() {
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ // Popular
+
+ override fun popularMangaRequest(page: Int): Request {
+ return GET("$baseUrl/popular-manga?page=$page")
+ }
+
+ override fun popularMangaSelector() = "div.media"
+
+ override fun popularMangaFromElement(element: Element): SManga {
+ return SManga.create().apply {
+ element.select("a:has(h4)").let {
+ setUrlWithoutDomain(it.attr("href"))
+ title = it.text()
+ }
+ thumbnail_url = element.select("img").attr("abs:src")
+ }
+ }
+
+ override fun popularMangaNextPageSelector() = "a[rel=next]"
+
+ // Latest
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$baseUrl/latest-manga?page=$page")
+ }
+
+ override fun latestUpdatesSelector() = popularMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+ // Search
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ return if (query.isNotBlank()) {
+ GET("$baseUrl/search?q=$query&page=$page")
+ } else {
+ val url = HttpUrl.parse("$baseUrl/mangas/")!!.newBuilder()
+ filters.forEach { filter ->
+ when (filter) {
+ is GenreFilter -> url.addPathSegment(filter.toUriPart())
+ is OrderFilter -> url.addQueryParameter("orderby", filter.toUriPart())
+ }
+ }
+ url.addQueryParameter("page", page.toString())
+ GET(url.toString(), headers)
+ }
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector()
+
+ override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+ // Manga details
+
+ override fun mangaDetailsParse(document: Document): SManga {
+ return SManga.create().apply {
+ title = document.select("div.manga-detail h1").text()
+ thumbnail_url = document.select("div.manga-detail img").attr("abs:src")
+ document.select("div.media-body p").html().split("
").forEach {
+ with(Jsoup.parse(it).text()) {
+ when {
+ this.startsWith("Author") -> author = this.substringAfter(":").trim()
+ this.startsWith("Artist") -> artist = this.substringAfter(":").trim()
+ this.startsWith("Genre") -> genre = this.substringAfter(":").trim().replace(";", ",")
+ this.startsWith("Status") -> status = this.substringAfter(":").trim().toStatus()
+ }
+ }
+ }
+ description = document.select("div.manga-content p").joinToString("\n") { it.text() }
+ }
+ }
+
+ private fun String?.toStatus() = when {
+ this == null -> SManga.UNKNOWN
+ this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
+ this.contains("Completed", ignoreCase = true) -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+
+ // Chapters
+
+ /**
+ * This theme has 3 chapter blocks: latest chapters with dates, all chapters without dates, and upcoming chapters
+ * Avoid parsing the upcoming chapters and filter out duplicate chapters
+ */
+
+ override fun chapterListParse(response: Response): List {
+ return super.chapterListParse(response).distinctBy { it.url }
+ }
+
+ override fun chapterListSelector() = "div.total-chapter:has(h2) li"
+
+ override fun chapterFromElement(element: Element): SChapter {
+ return SChapter.create().apply {
+ element.select("a").let {
+ name = it.text()
+ setUrlWithoutDomain(it.attr("href"))
+ }
+ date_upload = element.select("div.small").firstOrNull()?.text().toDate()
+ }
+ }
+
+ private val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
+
+ private fun String?.toDate(): Long {
+ this ?: return 0L
+ return try {
+ when {
+ this.contains("yesterday", ignoreCase = true) -> Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
+ this.contains("ago", ignoreCase = true) -> {
+ val trimmedDate = this.substringBefore(" ago").removeSuffix("s").split(" ")
+ val num = trimmedDate[0].toIntOrNull() ?: 1 // for "an hour ago"
+ val calendar = Calendar.getInstance()
+ when (trimmedDate[1]) {
+ "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -num) }
+ "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -num) }
+ "minute" -> calendar.apply { add(Calendar.MINUTE, -num) }
+ "second" -> calendar.apply { add(Calendar.SECOND, -num) }
+ else -> null
+ }?.timeInMillis ?: 0L
+ }
+ else -> SimpleDateFormat("MMM d yy", Locale.US)
+ .parse("${this.substringBefore(",")} $currentYear")
+ .time
+ }
+ } catch (_: Exception) {
+ 0L
+ }
+ }
+
+ // Pages
+
+ override fun pageListParse(document: Document): List {
+ return document.select("#arraydata").text().split(",").mapIndexed { i, url ->
+ Page(i, "", url)
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
+
+ // Filters
+
+ override fun getFilterList() = FilterList(
+ Filter.Header("NOTE: Ignored if using text search!"),
+ Filter.Separator(),
+ OrderFilter(getOrderList()),
+ GenreFilter(getGenreList())
+ )
+
+ private class OrderFilter(vals: Array>) : UriPartFilter("Category", vals)
+
+ private fun getOrderList() = arrayOf(
+ Pair("Views", "2"),
+ Pair("Latest", "3"),
+ Pair("A-Z", "1")
+ )
+
+ private class GenreFilter(vals: Array>) : UriPartFilter("Category", vals)
+
+ private fun getGenreList() = arrayOf(
+ Pair("4 koma", "4-koma"),
+ Pair("Action", "action"),
+ Pair("Adaptation", "adaptation"),
+ Pair("Adult", "adult"),
+ Pair("Adventure", "adventure"),
+ Pair("Aliens", "aliens"),
+ Pair("Animals", "animals"),
+ Pair("Anthology", "anthology"),
+ Pair("Award winning", "award-winning"),
+ Pair("Comedy", "comedy"),
+ Pair("Cooking", "cooking"),
+ Pair("Crime", "crime"),
+ Pair("Crossdressing", "crossdressing"),
+ Pair("Delinquents", "delinquents"),
+ Pair("Demons", "demons"),
+ Pair("Doujinshi", "doujinshi"),
+ Pair("Drama", "drama"),
+ Pair("Ecchi", "ecchi"),
+ Pair("Fan colored", "fan-colored"),
+ Pair("Fantasy", "fantasy"),
+ Pair("Food", "food"),
+ Pair("Full color", "full-color"),
+ Pair("Game", "game"),
+ Pair("Gender bender", "gender-bender"),
+ Pair("Genderswap", "genderswap"),
+ Pair("Ghosts", "ghosts"),
+ Pair("Gore", "gore"),
+ Pair("Gossip", "gossip"),
+ Pair("Gyaru", "gyaru"),
+ Pair("Harem", "harem"),
+ Pair("Historical", "historical"),
+ Pair("Horror", "horror"),
+ Pair("Isekai", "isekai"),
+ Pair("Josei", "josei"),
+ Pair("Kids", "kids"),
+ Pair("Loli", "loli"),
+ Pair("Lolicon", "lolicon"),
+ Pair("Long strip", "long-strip"),
+ Pair("Mafia", "mafia"),
+ Pair("Magic", "magic"),
+ Pair("Magical girls", "magical-girls"),
+ Pair("Manhwa", "manhwa"),
+ Pair("Martial arts", "martial-arts"),
+ Pair("Mature", "mature"),
+ Pair("Mecha", "mecha"),
+ Pair("Medical", "medical"),
+ Pair("Military", "military"),
+ Pair("Monster girls", "monster-girls"),
+ Pair("Monsters", "monsters"),
+ Pair("Music", "music"),
+ Pair("Mystery", "mystery"),
+ Pair("Ninja", "ninja"),
+ Pair("Office workers", "office-workers"),
+ Pair("Official colored", "official-colored"),
+ Pair("One shot", "one-shot"),
+ Pair("Parody", "parody"),
+ Pair("Philosophical", "philosophical"),
+ Pair("Police", "police"),
+ Pair("Post apocalyptic", "post-apocalyptic"),
+ Pair("Psychological", "psychological"),
+ Pair("Reincarnation", "reincarnation"),
+ Pair("Reverse harem", "reverse-harem"),
+ Pair("Romance", "romance"),
+ Pair("Samurai", "samurai"),
+ Pair("School life", "school-life"),
+ Pair("Sci fi", "sci-fi"),
+ Pair("Seinen", "seinen"),
+ Pair("Shota", "shota"),
+ Pair("Shotacon", "shotacon"),
+ Pair("Shoujo", "shoujo"),
+ Pair("Shoujo ai", "shoujo-ai"),
+ Pair("Shounen", "shounen"),
+ Pair("Shounen ai", "shounen-ai"),
+ Pair("Slice of life", "slice-of-life"),
+ Pair("Smut", "smut"),
+ Pair("Space", "space"),
+ Pair("Sports", "sports"),
+ Pair("Super power", "super-power"),
+ Pair("Superhero", "superhero"),
+ Pair("Supernatural", "supernatural"),
+ Pair("Survival", "survival"),
+ Pair("Suspense", "suspense"),
+ Pair("Thriller", "thriller"),
+ Pair("Time travel", "time-travel"),
+ Pair("Toomics", "toomics"),
+ Pair("Traditional games", "traditional-games"),
+ Pair("Tragedy", "tragedy"),
+ Pair("User created", "user-created"),
+ Pair("Vampire", "vampire"),
+ Pair("Vampires", "vampires"),
+ Pair("Video games", "video-games"),
+ Pair("Virtual reality", "virtual-reality"),
+ Pair("Web comic", "web-comic"),
+ Pair("Webtoon", "webtoon"),
+ Pair("Wuxia", "wuxia"),
+ Pair("Yaoi", "yaoi"),
+ Pair("Yuri", "yuri"),
+ Pair("Zombies", "zombies")
+ )
+
+ open class UriPartFilter(displayName: String, private val vals: Array>) :
+ Filter.Select(displayName, vals.map { it.first }.toTypedArray()) {
+ fun toUriPart() = vals[state].second
+ }
+}
diff --git a/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/PaprikaFactory.kt b/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/PaprikaFactory.kt
new file mode 100644
index 000000000..5706a5ac3
--- /dev/null
+++ b/src/all/paprika/src/eu/kanade/tachiyomi/extension/all/paprika/PaprikaFactory.kt
@@ -0,0 +1,14 @@
+package eu.kanade.tachiyomi.extension.all.paprika
+
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+
+class PaprikaFactory : SourceFactory {
+ override fun createSources(): List = listOf(
+ MangazukiXyz(),
+ MangaTensei()
+ )
+}
+
+class MangazukiXyz : Paprika("MangaZuki.xyz", "https://ir2me.com", "en")
+class MangaTensei : Paprika("MangaTensei", "https://www.mangatensei.com", "en")