Make ManhwaZ a multisrc + Add UmeTruyen (#1495)
* Make ManhwaZ a multisrc + Add UmeTruyen * Forgot to commit the most important stuff * icons * Update src/en/manhwaz/build.gradle Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Don't use GlobalScope * Remove useless optin * Add CoroutineScope import --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
parent
6d16a8908c
commit
a95fa2dd5c
|
@ -0,0 +1,12 @@
|
||||||
|
filter_ignored_warning=Ignored when using text search
|
||||||
|
cannot_use_order_by_warning=Cannot use "Order by" filter when genre is "%s" or "%s"
|
||||||
|
genre_fetch_failed=Failed to fetch genres
|
||||||
|
genre_missing_warning=Press "Reset" to attempt to show genres
|
||||||
|
genre_filter_title=Genre
|
||||||
|
genre_all=All
|
||||||
|
genre_completed=Completed
|
||||||
|
order_by_filter_title=Order by
|
||||||
|
order_by_latest=Latest
|
||||||
|
order_by_rating=Rating
|
||||||
|
order_by_most_views=Most views
|
||||||
|
order_by_new=New
|
|
@ -0,0 +1,12 @@
|
||||||
|
filter_ignored_warning=Không thể dùng chung với tìm kiếm bằng từ khoá
|
||||||
|
cannot_use_order_by_warning=Không thể sắp xếp nếu chọn thể loại là "%s" hoặc "%s"
|
||||||
|
genre_fetch_failed=Đã có lỗi khi tải thể loại
|
||||||
|
genre_missing_warning=Chọn "Đặt lại" để hiển thị thể loại
|
||||||
|
genre_filter_title=Thể loại
|
||||||
|
genre_all=Tất cả
|
||||||
|
genre_completed=Hoàn thành
|
||||||
|
order_by_filter_title=Sắp xếp theo
|
||||||
|
order_by_latest=Mới nhất
|
||||||
|
order_by_rating=Đánh giá cao
|
||||||
|
order_by_most_views=Xem nhiều
|
||||||
|
order_by_new=Mới
|
|
@ -0,0 +1,9 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(project(":lib:i18n"))
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.manhwaz
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
abstract class ManhwaZ(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String,
|
||||||
|
private val mangaDetailsAuthorHeading: String = "author(s)",
|
||||||
|
private val mangaDetailsStatusHeading: String = "status",
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Origin", baseUrl)
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
protected val intl = Intl(
|
||||||
|
lang,
|
||||||
|
setOf("en", "vi"),
|
||||||
|
"en",
|
||||||
|
this::class.java.classLoader!!,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "#slide-top > .item"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
element.selectFirst(".info-item a")!!.let {
|
||||||
|
title = it.text()
|
||||||
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
}
|
||||||
|
thumbnail_url = element.selectFirst(".img-item img")?.imgAttr()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = ".page-item-detail"
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
||||||
|
element.selectFirst(".item-summary a")!!.let {
|
||||||
|
title = it.text()
|
||||||
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
}
|
||||||
|
thumbnail_url = element.selectFirst(".item-thumb img")?.imgAttr()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String? = "ul.pager a[rel=next]"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("s", query)
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
val filterList = filters.ifEmpty { getFilterList() }
|
||||||
|
val genreFilter = filterList.find { it is GenreFilter } as? GenreFilter
|
||||||
|
val orderByFilter = filterList.find { it is OrderByFilter } as? OrderByFilter
|
||||||
|
val genreId = genreFilter?.options?.get(genreFilter.state)?.id
|
||||||
|
|
||||||
|
if (genreFilter != null && genreFilter.state != 0) {
|
||||||
|
addPathSegments(genreId!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't sort in "All" or "Completed"
|
||||||
|
if (orderByFilter != null && genreId?.startsWith("genre/") == true) {
|
||||||
|
addQueryParameter(
|
||||||
|
"m_orderby",
|
||||||
|
orderByFilter.options[orderByFilter.state].id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String? = latestUpdatesNextPageSelector()
|
||||||
|
|
||||||
|
private val ongoingStatusList = listOf("ongoing", "đang ra")
|
||||||
|
private val completedStatusList = listOf("completed", "hoàn thành")
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
val statusText = document.selectFirst("div.summary-heading:contains($mangaDetailsStatusHeading) + div.summary-content")
|
||||||
|
?.text()
|
||||||
|
?.lowercase()
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
title = document.selectFirst("div.post-title h1")!!.text()
|
||||||
|
author = document.selectFirst("div.summary-heading:contains($mangaDetailsAuthorHeading) + div.summary-content")?.text()
|
||||||
|
description = document.selectFirst("div.summary__content")?.text()
|
||||||
|
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
|
||||||
|
status = when {
|
||||||
|
ongoingStatusList.contains(statusText) -> SManga.ONGOING
|
||||||
|
completedStatusList.contains(statusText) -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst("div.summary_image img")?.imgAttr()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "li.wp-manga-chapter"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
element.selectFirst("a")!!.let {
|
||||||
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
name = it.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
element.selectFirst("span.chapter-release-date")?.text()?.let {
|
||||||
|
date_upload = parseRelativeDate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document) =
|
||||||
|
document.select("div.page-break img").mapIndexed { i, it ->
|
||||||
|
Page(i, imageUrl = it.imgAttr())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
fetchGenreList()
|
||||||
|
|
||||||
|
val filters = buildList {
|
||||||
|
add(Filter.Header(intl["filter_ignored_warning"]))
|
||||||
|
add(Filter.Header(intl.format("cannot_use_order_by_warning", intl["genre_all"], intl["genre_completed"])))
|
||||||
|
|
||||||
|
if (fetchGenreStatus == FetchGenreStatus.NOT_FETCHED && fetchGenreAttempts >= 3) {
|
||||||
|
add(Filter.Header(intl["genre_fetch_failed"]))
|
||||||
|
} else if (fetchGenreStatus != FetchGenreStatus.FETCHED) {
|
||||||
|
add(Filter.Header(intl["genre_missing_warning"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(Filter.Separator())
|
||||||
|
if (genres.isNotEmpty()) {
|
||||||
|
add(GenreFilter(intl, genres))
|
||||||
|
}
|
||||||
|
add(OrderByFilter(intl))
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GenreFilter(
|
||||||
|
intl: Intl,
|
||||||
|
genres: List<SelectOption>,
|
||||||
|
) : SelectFilter(intl["genre_filter_title"], genres)
|
||||||
|
|
||||||
|
private class OrderByFilter(intl: Intl) : SelectFilter(
|
||||||
|
intl["order_by_filter_title"],
|
||||||
|
listOf(
|
||||||
|
SelectOption(intl["order_by_latest"], "latest"),
|
||||||
|
SelectOption(intl["order_by_rating"], "rating"),
|
||||||
|
SelectOption(intl["order_by_most_views"], "views"),
|
||||||
|
SelectOption(intl["order_by_new"], "new"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private var genres = emptyList<SelectOption>()
|
||||||
|
private var fetchGenreStatus = FetchGenreStatus.NOT_FETCHED
|
||||||
|
private var fetchGenreAttempts = 0
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
private fun fetchGenreList() {
|
||||||
|
if (fetchGenreStatus != FetchGenreStatus.NOT_FETCHED || fetchGenreAttempts >= 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchGenreStatus = FetchGenreStatus.FETCHING
|
||||||
|
fetchGenreAttempts++
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val document = client.newCall(GET("$baseUrl/genre")).await().asJsoup()
|
||||||
|
|
||||||
|
genres = buildList {
|
||||||
|
add(SelectOption(intl["genre_all"], ""))
|
||||||
|
add(SelectOption(intl["genre_completed"], "completed"))
|
||||||
|
document.select("ul.page-genres li a").forEach {
|
||||||
|
val path = it.absUrl("href").toHttpUrl().encodedPath.removePrefix("/")
|
||||||
|
|
||||||
|
add(SelectOption(it.ownText(), path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchGenreStatus = FetchGenreStatus.FETCHED
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ManhwaZ/$name", "Error fetching genres", e)
|
||||||
|
fetchGenreStatus = FetchGenreStatus.NOT_FETCHED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class FetchGenreStatus { NOT_FETCHED, FETCHED, FETCHING }
|
||||||
|
|
||||||
|
private class SelectOption(val name: String, val id: String)
|
||||||
|
|
||||||
|
private open class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
val options: List<SelectOption>,
|
||||||
|
) : Filter.Select<String>(name, options.map { it.name }.toTypedArray())
|
||||||
|
|
||||||
|
private val secondsUnit = listOf("second", "seconds", "giây")
|
||||||
|
private val minutesUnit = listOf("minute", "minutes", "phút")
|
||||||
|
private val hourUnit = listOf("hour", "hours", "giờ")
|
||||||
|
private val dayUnit = listOf("day", "days", "ngày")
|
||||||
|
private val weekUnit = listOf("week", "weeks", "tuần")
|
||||||
|
private val monthUnit = listOf("month", "months", "tháng")
|
||||||
|
private val yearUnit = listOf("year", "years", "năm")
|
||||||
|
|
||||||
|
private fun parseRelativeDate(date: String): Long {
|
||||||
|
val (valueString, unit) = date.substringBeforeLast(" ").split(" ", limit = 2)
|
||||||
|
val value = valueString.toInt()
|
||||||
|
|
||||||
|
val calendar = Calendar.getInstance().apply {
|
||||||
|
val field = when {
|
||||||
|
secondsUnit.contains(unit) -> Calendar.SECOND
|
||||||
|
minutesUnit.contains(unit) -> Calendar.MINUTE
|
||||||
|
hourUnit.contains(unit) -> Calendar.HOUR_OF_DAY
|
||||||
|
dayUnit.contains(unit) -> Calendar.DAY_OF_MONTH
|
||||||
|
weekUnit.contains(unit) -> Calendar.WEEK_OF_MONTH
|
||||||
|
monthUnit.contains(unit) -> Calendar.MONTH
|
||||||
|
yearUnit.contains(unit) -> Calendar.YEAR
|
||||||
|
else -> return 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
add(field, -value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendar.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
|
||||||
|
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ManhwaZ'
|
extName = 'ManhwaZ'
|
||||||
extClass = '.ManhwaZ'
|
extClass = '.ManhwaZCom'
|
||||||
themePkg = 'madara'
|
themePkg = 'manhwaz'
|
||||||
baseUrl = 'https://manhwaz.com'
|
baseUrl = 'https://manhwaz.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 35
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manhwaz
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
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.SManga
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class ManhwaZ : Madara(
|
|
||||||
"ManhwaZ",
|
|
||||||
"https://manhwaz.com",
|
|
||||||
"en",
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override val fetchGenres = false
|
|
||||||
|
|
||||||
override val useNewChapterEndpoint = true
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/", headers)
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = "div#slide-top > div.item"
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
thumbnail_url = element.selectFirst(".img-item img")?.let(::imageFromElement) ?: ""
|
|
||||||
element.selectFirst(".info-item a")!!.run {
|
|
||||||
title = text().trim()
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = ".manga-content > div.row > div"
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
thumbnail_url = element.selectFirst(".item-thumb img")?.let(::imageFromElement) ?: ""
|
|
||||||
element.selectFirst(".item-summary a")!!.run {
|
|
||||||
title = text().trim()
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li.active + li:not(.disabled)"
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter("s", query)
|
|
||||||
} else {
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> {
|
|
||||||
if (filter.selected == null) throw Exception("Must select a genre")
|
|
||||||
addPathSegment("genre")
|
|
||||||
addPathSegment(filter.selected!!)
|
|
||||||
}
|
|
||||||
is OrderFilter -> {
|
|
||||||
addQueryParameter("m_orderby", filter.selected)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
return if (response.request.url.encodedPath.startsWith("/search")) {
|
|
||||||
searchParse(response)
|
|
||||||
} else {
|
|
||||||
super.searchMangaParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector(): String = "div.listing > div"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String = latestUpdatesNextPageSelector()
|
|
||||||
|
|
||||||
private fun searchParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val mangaList = document.select(".page-search > .container > .row > div")
|
|
||||||
.map(::searchMangaFromElement)
|
|
||||||
|
|
||||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
|
||||||
|
|
||||||
return MangasPage(mangaList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
|
|
||||||
abstract class SelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val options: List<Pair<String, String>>,
|
|
||||||
defaultValue: String? = null,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
options.map { it.first }.toTypedArray(),
|
|
||||||
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
|
||||||
) {
|
|
||||||
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrderFilter : SelectFilter(
|
|
||||||
"Order By",
|
|
||||||
listOf(
|
|
||||||
Pair("Latest", "latest"),
|
|
||||||
Pair("Rating", "rating"),
|
|
||||||
Pair("Most Views", "views"),
|
|
||||||
Pair("New", "new"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class GenreFilter : SelectFilter(
|
|
||||||
"Genre",
|
|
||||||
listOf(
|
|
||||||
Pair("<select>", ""),
|
|
||||||
Pair("Action", "action"),
|
|
||||||
Pair("Adult", "adult"),
|
|
||||||
Pair("Adventure", "adventure"),
|
|
||||||
Pair("Comedy", "comedy"),
|
|
||||||
Pair("Cooking", "cooking"),
|
|
||||||
Pair("Detective", "detective"),
|
|
||||||
Pair("Doujinshi", "doujinshi"),
|
|
||||||
Pair("Drama", "drama"),
|
|
||||||
Pair("Ecchi", "ecchi"),
|
|
||||||
Pair("Fantasy", "fantasy"),
|
|
||||||
Pair("Gender Bender", "gender-bender"),
|
|
||||||
Pair("Harem", "harem"),
|
|
||||||
Pair("Historical", "historical"),
|
|
||||||
Pair("Horror", "horror"),
|
|
||||||
Pair("Isekai", "isekai"),
|
|
||||||
Pair("Josei", "josei"),
|
|
||||||
Pair("Manga", "manga"),
|
|
||||||
Pair("Manhua", "manhua"),
|
|
||||||
Pair("Manhwa", "manhwa"),
|
|
||||||
Pair("Martial Arts", "martial-arts"),
|
|
||||||
Pair("Mature", "mature"),
|
|
||||||
Pair("Mecha", "mecha"),
|
|
||||||
Pair("Mystery", "mystery"),
|
|
||||||
Pair("One shot", "one-shot"),
|
|
||||||
Pair("Psychological", "psychological"),
|
|
||||||
Pair("Romance", "romance"),
|
|
||||||
Pair("School Life", "school-life"),
|
|
||||||
Pair("Sci-fi", "sci-fi"),
|
|
||||||
Pair("Seinen", "seinen"),
|
|
||||||
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("Sports", "sports"),
|
|
||||||
Pair("Supernatural", "supernatural"),
|
|
||||||
Pair("Tragedy", "tragedy"),
|
|
||||||
Pair("Webtoon", "webtoon"),
|
|
||||||
Pair("Yaoi", "yaoi"),
|
|
||||||
Pair("Yuri", "yuri"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
|
||||||
Filter.Separator(),
|
|
||||||
GenreFilter(),
|
|
||||||
OrderFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override val mangaDetailsSelectorStatus = ".post-content_item:contains(status) .summary-content"
|
|
||||||
|
|
||||||
override val mangaDetailsSelectorAuthor = ".post-content_item:contains(Author) .summary-content"
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.manhwaz
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class ManhwaZCom : ManhwaZ(
|
||||||
|
"ManhwaZ",
|
||||||
|
"https://manhwaz.com",
|
||||||
|
"en",
|
||||||
|
) {
|
||||||
|
override val client: OkHttpClient = super.client.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = "SayHentai"
|
extName = "SayHentai"
|
||||||
extClass = ".SayHentai"
|
extClass = ".SayHentai"
|
||||||
extVersionCode = 3
|
themePkg = "manhwaz"
|
||||||
|
baseUrl = "https://sayhentai.club"
|
||||||
|
overrideVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,225 +1,11 @@
|
||||||
package eu.kanade.tachiyomi.extension.vi.sayhentai
|
package eu.kanade.tachiyomi.extension.vi.sayhentai
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
|
||||||
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 okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
// This is basically Madara CSS without the actual Madara bits, grrr
|
class SayHentai : ManhwaZ(
|
||||||
class SayHentai : ParsedHttpSource() {
|
"SayHentai",
|
||||||
|
"https://sayhentai.club",
|
||||||
override val name = "SayHentai"
|
"vi",
|
||||||
|
mangaDetailsAuthorHeading = "Tác giả",
|
||||||
override val lang = "vi"
|
mangaDetailsStatusHeading = "Trạng thái",
|
||||||
|
)
|
||||||
override val baseUrl = "https://sayhentai.club"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Origin", baseUrl)
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/?page=$page")
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.page-item-detail"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
val a = element.selectFirst("a")!!
|
|
||||||
|
|
||||||
setUrlWithoutDomain(a.attr("abs:href"))
|
|
||||||
title = a.attr("title")
|
|
||||||
thumbnail_url = element.selectFirst("img")?.imageFromElement()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "ul.pager a[rel=next]"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter("s", query)
|
|
||||||
} else {
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach {
|
|
||||||
when (it) {
|
|
||||||
is GenreList -> addPathSegments(it.values[it.state].path)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("div.post-title h1")!!.text()
|
|
||||||
author = document.selectFirst("div.summary-heading:contains(Tác giả) + div.summary-content")?.text()
|
|
||||||
description = document.selectFirst("div.summary__content")?.text()
|
|
||||||
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
|
|
||||||
status = when (document.selectFirst("div.summary-heading:contains(Trạng thái) + div.summary-content")?.text()) {
|
|
||||||
"Đang Ra" -> SManga.ONGOING
|
|
||||||
"Hoàn Thành" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
thumbnail_url = document.selectFirst("div.summary_image img")?.imageFromElement()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "li.wp-manga-chapter"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
val a = element.selectFirst("a")!!
|
|
||||||
val date = element.selectFirst("span.chapter-release-date")?.text()
|
|
||||||
|
|
||||||
setUrlWithoutDomain(a.attr("abs:href"))
|
|
||||||
name = a.text()
|
|
||||||
|
|
||||||
if (date != null) {
|
|
||||||
date_upload = parseRelativeDate(date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("div.page-break img").mapIndexed { i, it ->
|
|
||||||
Page(i, imageUrl = it.imageFromElement())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("Không dùng chung với tìm kiếm bằng từ khoá."),
|
|
||||||
GenreList(getGenreList()),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Element.imageFromElement(): String? {
|
|
||||||
return when {
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRelativeDate(date: String): Long {
|
|
||||||
val (valueString, unit) = date.substringBefore(" trước").split(" ")
|
|
||||||
val value = valueString.toInt()
|
|
||||||
|
|
||||||
val calendar = Calendar.getInstance().apply {
|
|
||||||
when (unit) {
|
|
||||||
"giây" -> add(Calendar.SECOND, -value)
|
|
||||||
"phút" -> add(Calendar.MINUTE, -value)
|
|
||||||
"giờ" -> add(Calendar.HOUR_OF_DAY, -value)
|
|
||||||
"ngày" -> add(Calendar.DAY_OF_MONTH, -value)
|
|
||||||
"tuần" -> add(Calendar.WEEK_OF_MONTH, -value)
|
|
||||||
"tháng" -> add(Calendar.MONTH, -value)
|
|
||||||
"năm" -> add(Calendar.YEAR, -value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return calendar.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
// document.querySelectorAll("span.number-story").forEach((e) => e.remove())
|
|
||||||
// copy([...document.querySelectorAll(".page-category ul li a")].map((e) => `Genre("${e.textContent.trim()}", "${e.href.replace("https://sayhentai.club/", "")}"),`).join("\n"))
|
|
||||||
//
|
|
||||||
// There are 2 pseudo-genres: Tất cả (All), and Hoàn thành (Completed), mostly for convenience.
|
|
||||||
private fun getGenreList() = arrayOf(
|
|
||||||
Genre("Tất cả", ""),
|
|
||||||
Genre("18+", "genre/18"),
|
|
||||||
Genre("3D", "genre/3d"),
|
|
||||||
Genre("Action", "genre/action"),
|
|
||||||
Genre("Adult", "genre/adult"),
|
|
||||||
Genre("Bạo Dâm", "genre/bao-dam"),
|
|
||||||
Genre("Chơi Hai Lỗ", "genre/choi-hai-lo"),
|
|
||||||
Genre("Comedy", "genre/comedy"),
|
|
||||||
Genre("Detective", "genre/detective"),
|
|
||||||
Genre("Doujinshi", "genre/doujinshi"),
|
|
||||||
Genre("Drama", "genre/drama"),
|
|
||||||
Genre("Ecchi", "genre/ecchi"),
|
|
||||||
Genre("Fantasy", "genre/fantasy"),
|
|
||||||
Genre("Gangbang", "genre/gangbang"),
|
|
||||||
Genre("Gender Bender", "genre/gender-bender"),
|
|
||||||
Genre("Giáo Viên", "genre/giao-vien"),
|
|
||||||
Genre("Group", "genre/group"),
|
|
||||||
Genre("Hãm Hiếp", "genre/ham-hiep"),
|
|
||||||
Genre("Harem", "genre/harem"),
|
|
||||||
Genre("Hậu Môn", "genre/hau-mon"),
|
|
||||||
Genre("Historical", "genre/historical"),
|
|
||||||
Genre("Hoàn thành", "completed"),
|
|
||||||
Genre("Horror", "genre/horror"),
|
|
||||||
Genre("Housewife", "genre/housewife"),
|
|
||||||
Genre("Josei", "genre/josei"),
|
|
||||||
Genre("Không Che", "genre/khong-che"),
|
|
||||||
Genre("Kinh Dị", "genre/kinh-di"),
|
|
||||||
Genre("Lão Già Dâm", "genre/lao-gia-dam"),
|
|
||||||
Genre("Loạn Luân", "genre/loan-luan"),
|
|
||||||
Genre("Loli", "genre/loli"),
|
|
||||||
Genre("Manga", "genre/manga"),
|
|
||||||
Genre("Manhua", "genre/manhua"),
|
|
||||||
Genre("Manhwa", "genre/manhwa"),
|
|
||||||
Genre("Martial Arts", "genre/martial-arts"),
|
|
||||||
Genre("Mature", "genre/mature"),
|
|
||||||
Genre("Milf", "genre/milf"),
|
|
||||||
Genre("Mind Break", "genre/mind-break"),
|
|
||||||
Genre("Mystery", "genre/mystery"),
|
|
||||||
Genre("Ngực Lớn", "genre/nguc-lon"),
|
|
||||||
Genre("Ngực Nhỏ", "genre/nguc-nho"),
|
|
||||||
Genre("Nô Lệ", "genre/no-le"),
|
|
||||||
Genre("NTR", "genre/ntr"),
|
|
||||||
Genre("Nữ Sinh", "genre/nu-sinh"),
|
|
||||||
Genre("Old Man", "genre/old-man"),
|
|
||||||
Genre("One shot", "genre/one-shot"),
|
|
||||||
Genre("Oneshot", "genre/oneshot"),
|
|
||||||
Genre("Psychological", "genre/psychological"),
|
|
||||||
Genre("Rape", "genre/rape"),
|
|
||||||
Genre("Romance", "genre/romance"),
|
|
||||||
Genre("School Life", "genre/school-life"),
|
|
||||||
Genre("Sci-fi", "genre/sci-fi"),
|
|
||||||
Genre("Seinen", "genre/seinen"),
|
|
||||||
Genre("Series", "genre/series"),
|
|
||||||
Genre("Shoujo", "genre/shoujo"),
|
|
||||||
Genre("Shoujo Ai", "genre/shoujo-ai"),
|
|
||||||
Genre("Shounen", "genre/shounen"),
|
|
||||||
Genre("Slice of Life", "genre/slice-of-life"),
|
|
||||||
Genre("Smut", "genre/smut"),
|
|
||||||
Genre("Sports", "genre/sports"),
|
|
||||||
Genre("Supernatural", "genre/supernatural"),
|
|
||||||
Genre("Tragedy", "genre/tragedy"),
|
|
||||||
Genre("Virgin", "genre/virgin"),
|
|
||||||
Genre("Webtoon", "genre/webtoon"),
|
|
||||||
Genre("Y Tá", "genre/y-ta"),
|
|
||||||
Genre("Yaoi", "genre/yaoi"),
|
|
||||||
Genre("Yuri", "genre/yuri"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class Genre(val name: String, val path: String) {
|
|
||||||
override fun toString() = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Thể loại", genres)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
ext {
|
||||||
|
extName = "UmeTruyen"
|
||||||
|
extClass = ".UmeTruyen"
|
||||||
|
themePkg = "manhwaz"
|
||||||
|
baseUrl = "https://umetruyenvip.com"
|
||||||
|
overrideVersionCode = 0
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.vi.umetruyen
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
|
||||||
|
|
||||||
|
class UmeTruyen : ManhwaZ(
|
||||||
|
"UmeTruyen",
|
||||||
|
"https://umetruyenvip.com",
|
||||||
|
"vi",
|
||||||
|
mangaDetailsAuthorHeading = "Tác giả",
|
||||||
|
mangaDetailsStatusHeading = "Trạng thái",
|
||||||
|
)
|
Loading…
Reference in New Issue