Add ManhuaPlusOrg (#578)

This commit is contained in:
NotBlankyu 2024-01-24 19:08:50 +00:00 committed by Draff
parent 85af4c5f97
commit 8938b92e09
9 changed files with 390 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,7 @@
ext {
extName = 'ManhuaPlus (unoriginal)'
extClass = '.ManhuaPlusOrg'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,242 @@
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
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
class ManhuaPlusOrg : ParsedHttpSource() {
override val name = "ManhuaPlus (Unoriginal)"
override val baseUrl = "https://manhuaplus.org"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// Popular
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
override fun popularMangaSelector(): String = "div#main div.grid > div"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst("img")?.imgAttr()
element.selectFirst(".text-center a")!!.run {
title = text().trim()
setUrlWithoutDomain(attr("href"))
}
}
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
// Latest
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/all-manga/$page/?sort=1", headers)
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
override fun latestUpdatesSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SManga =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment("search")
addQueryParameter("keyword", query)
} else {
addPathSegment("filter")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
if (filter.checked.isNotEmpty()) {
addQueryParameter("genres", filter.checked.joinToString(","))
}
}
is StatusFilter -> {
if (filter.selected.isNotBlank()) {
addQueryParameter("status", filter.selected)
}
}
is SortFilter -> {
addQueryParameter("sort", filter.selected)
}
is ChapterCountFilter -> {
addQueryParameter("chapter_count", filter.selected)
}
is GenderFilter -> {
addQueryParameter("sex", filter.selected)
}
else -> {}
}
}
}
addPathSegment(page.toString())
addPathSegment("")
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun searchMangaSelector(): String =
throw UnsupportedOperationException()
override fun searchMangaFromElement(element: Element): SManga =
throw UnsupportedOperationException()
override fun searchMangaNextPageSelector(): String =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Ignored when using text search"),
Filter.Separator(),
GenreFilter(),
ChapterCountFilter(),
GenderFilter(),
StatusFilter(),
SortFilter(),
)
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
description = document.selectFirst("div#syn-target")?.text()
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A"
genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() }
document.selectFirst(".a1 > aside")?.run {
author = select("div:contains(Authors) > span a")
.joinToString(", ") { it.text().trim() }
.takeUnless { it.isBlank() || it.equals("Updating", true) }
status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus)
}
}
private fun parseStatus(status: String?): Int = when {
status.equals("ongoing", true) -> SManga.ONGOING
status.equals("completed", true) -> SManga.COMPLETED
status.equals("on-hold", true) -> SManga.ON_HIATUS
status.equals("canceled", true) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListSelector() = "ul > li.chapter"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst("time[datetime]")?.also {
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
}
element.selectFirst("a")!!.run {
text().trim().also {
name = it
chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F
}
setUrlWithoutDomain(attr("href"))
}
}
override fun pageListRequest(chapter: SChapter): Request {
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
val pageHeaders = headersBuilder().apply {
add("Accept", "application/json, text/javascript, *//*; q=0.01")
add("Host", baseUrl.toHttpUrl().host)
add("Referer", baseUrl + chapter.url)
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders)
}
@Serializable
data class PageListResponseDto(val html: String)
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<PageListResponseDto>().html
return pageListParse(
Jsoup.parseBodyFragment(
data,
response.request.header("Referer")!!,
),
)
}
override fun pageListParse(document: Document): List<Page> {
return document.select("div.separator").map { page ->
val index = page.selectFirst("img")!!.attr("alt").substringAfterLast(" ").toInt()
val url = page.selectFirst("a")!!.attr("abs:href")
Page(index, document.location(), url)
}.sortedBy { it.index }
}
override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, imgHeaders)
}
// Utilities
// From mangathemesia
private fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
else -> attr("abs:src")
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
}

View File

@ -0,0 +1,139 @@
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
import eu.kanade.tachiyomi.source.model.Filter
abstract class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
) : Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
) {
val selected get() = options[state].second
}
class CheckBoxFilter(
name: String,
val value: String,
) : Filter.CheckBox(name)
class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) {
companion object {
private val chapterCount = listOf(
Pair(">= 0", "0"),
Pair(">= 10", "10"),
Pair(">= 30", "30"),
Pair(">= 50", "50"),
Pair(">= 100", "100"),
Pair(">= 200", "200"),
Pair(">= 300", "300"),
Pair(">= 400", "400"),
Pair(">= 500", "500"),
)
}
}
class GenderFilter : SelectFilter("Manga Gender", gender) {
companion object {
private val gender = listOf(
Pair("All", "All"),
Pair("Boy", "Boy"),
Pair("Girl", "Girl"),
)
}
}
class StatusFilter : SelectFilter("Status", status) {
companion object {
private val status = listOf(
Pair("All", ""),
Pair("Completed", "completed"),
Pair("OnGoing", "on-going"),
Pair("On-Hold", "on-hold"),
Pair("Canceled", "canceled"),
)
}
}
class SortFilter : SelectFilter("Sort", sort) {
companion object {
private val sort = listOf(
Pair("Default", "default"),
Pair("Latest Updated", "latest-updated"),
Pair("Most Viewed", "views"),
Pair("Most Viewed Month", "views_month"),
Pair("Most Viewed Week", "views_week"),
Pair("Most Viewed Day", "views_day"),
Pair("Score", "score"),
Pair("Name A-Z", "az"),
Pair("Name Z-A", "za"),
Pair("Newest", "new"),
Pair("Oldest", "old"),
)
}
}
class GenreFilter : Filter.Group<CheckBoxFilter>(
"Genre",
genres.map { CheckBoxFilter(it.first, it.second) },
) {
val checked get() = state.filter { it.state }.map { it.value }
companion object {
private val genres = listOf(
Pair("Action", "4"),
Pair("Adaptation", "87"),
Pair("Adult", "31"),
Pair("Adventure", "5"),
Pair("Animals", "1657"),
Pair("Cartoon", "46"),
Pair("Comedy", "14"),
Pair("Demons", "284"),
Pair("Drama", "59"),
Pair("Ecchi", "67"),
Pair("Fantasy", "6"),
Pair("Full Color", "89"),
Pair("Genderswap", "2409"),
Pair("Ghosts", "2253"),
Pair("Gore", "1182"),
Pair("Harem", "17"),
Pair("Historical", "642"),
Pair("Horror", "797"),
Pair("Isekai", "239"),
Pair("Live action", "11"),
Pair("Long Strip", "86"),
Pair("Magic", "90"),
Pair("Magical Girls", "1470"),
Pair("Manhua", "7"),
Pair("Manhwa", "70"),
Pair("Martial Arts", "8"),
Pair("Mature", "12"),
Pair("Mecha", "786"),
Pair("Medical", "1443"),
Pair("Monsters", "138"),
Pair("Mystery", "9"),
Pair("Post-Apocalyptic", "285"),
Pair("Psychological", "798"),
Pair("Reincarnation", "139"),
Pair("Romance", "987"),
Pair("School Life", "10"),
Pair("Sci-fi", "135"),
Pair("Seinen", "196"),
Pair("Shounen", "26"),
Pair("Shounen ai", "64"),
Pair("Slice of Life", "197"),
Pair("Superhero", "136"),
Pair("Supernatural", "13"),
Pair("Survival", "140"),
Pair("Thriller", "137"),
Pair("Time travel", "231"),
Pair("Tragedy", "15"),
Pair("Video Games", "283"),
Pair("Villainess", "676"),
Pair("Virtual Reality", "611"),
Pair("Web comic", "88"),
Pair("Webtoon", "18"),
Pair("Wuxia", "239"),
)
}
}