Add ManhuaPlusOrg (#578)
This commit is contained in:
parent
85af4c5f97
commit
8938b92e09
2
src/en/manhuaplusorg/AndroidManifest.xml
Normal file
2
src/en/manhuaplusorg/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
7
src/en/manhuaplusorg/build.gradle
Normal file
7
src/en/manhuaplusorg/build.gradle
Normal file
@ -0,0 +1,7 @@
|
||||
ext {
|
||||
extName = 'ManhuaPlus (unoriginal)'
|
||||
extClass = '.ManhuaPlusOrg'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/manhuaplusorg/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/manhuaplusorg/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
src/en/manhuaplusorg/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/manhuaplusorg/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
src/en/manhuaplusorg/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/manhuaplusorg/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
src/en/manhuaplusorg/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/manhuaplusorg/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
BIN
src/en/manhuaplusorg/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/manhuaplusorg/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -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())
|
||||
}
|
||||
}
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user