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")