TruyenHentai18: Update domain and fix loading content (#9586)
* Update domains * Add private statement in DTO * Add setUrlWithoutDomain in mangaDetailsParse * Save slug without lang prefix * Apply changes
This commit is contained in:
parent
b747b55681
commit
03b8b9b4ca
@ -12,8 +12,8 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="truyenhentai18.pro"
|
android:host="truyenhentai18.app"
|
||||||
android:pathPattern="/..*\\.html"
|
android:pathPattern="/vi/..*\\.html"
|
||||||
android:scheme="https"/>
|
android:scheme="https"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = "Truyen Hentai 18+"
|
extName = "Truyen Hentai 18+"
|
||||||
extClass = ".TruyenHentai18"
|
extClass = ".TruyenHentai18"
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,18 +7,23 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
class TruyenHentai18 : ParsedHttpSource() {
|
class TruyenHentai18 : ParsedHttpSource() {
|
||||||
|
|
||||||
override val name = "Truyện Hentai 18+"
|
override val name = "Truyện Hentai 18+"
|
||||||
|
|
||||||
override val baseUrl = "https://truyenhentai18.pro"
|
override val baseUrl = "https://truyenhentai18.app"
|
||||||
|
|
||||||
|
private val apiUrl = "https://api.th18.app"
|
||||||
|
|
||||||
override val lang = "vi"
|
override val lang = "vi"
|
||||||
|
|
||||||
@ -29,24 +34,29 @@ class TruyenHentai18 : ParsedHttpSource() {
|
|||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
// ============================== Popular ======================================
|
||||||
GET("$baseUrl/truyen-de-xuat" + if (page > 1) "/page/$page" else "", headers)
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.row > div[class^=item-] > div.card"
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$baseUrl/$lang/truyen-de-xuat" + if (page > 1) "/page/$page" else "", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".container .p-2 .shadow-sm.overflow-hidden"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
element.selectFirst("a.item-title")!!.let {
|
element.selectFirst("a[title]")!!.let {
|
||||||
setUrlWithoutDomain(it.attr("href"))
|
setUrlWithoutDomain(it.absUrl("href"))
|
||||||
title = it.text()
|
url = url.removePrefix("/$lang")
|
||||||
|
title = it.attr("title")
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail_url = element.selectFirst("a.item-cover img")?.absUrl("data-src")
|
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "ul.pagination li.page-item.active:not(:last-child)"
|
override fun popularMangaNextPageSelector() = "ul.pagination li.page-item.active:not(:last-child)"
|
||||||
|
|
||||||
|
// ============================== Latest ======================================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
GET("$baseUrl/truyen-moi" + if (page > 1) "/page/$page" else "", headers)
|
GET("$baseUrl/$lang/truyen-moi" + if (page > 1) "/page/$page" else "", headers)
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
@ -54,97 +64,119 @@ class TruyenHentai18 : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
override fun fetchSearchManga(
|
// ============================== Search ======================================
|
||||||
page: Int,
|
|
||||||
query: String,
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
filters: FilterList,
|
|
||||||
): Observable<MangasPage> {
|
|
||||||
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
|
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
|
||||||
val slug = query.removePrefix(PREFIX_SLUG_SEARCH)
|
val slug = query.removePrefix(PREFIX_SLUG_SEARCH)
|
||||||
val url = "/$slug"
|
fetchMangaDetails(SManga.create().apply { this.url = "/$lang/$slug" })
|
||||||
|
.map { MangasPage(listOf(it), false) }
|
||||||
fetchMangaDetails(SManga.create().apply { this.url = url })
|
|
||||||
.map { MangasPage(listOf(it.apply { this.url = url }), false) }
|
|
||||||
} else {
|
} else {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = "$apiUrl/posts".toHttpUrl().newBuilder()
|
||||||
if (page > 1) {
|
.addQueryParameter("language", lang)
|
||||||
addPathSegment("page")
|
.addQueryParameter("order", "latest")
|
||||||
addPathSegment(page.toString())
|
.addQueryParameter("status", "taxonomyid")
|
||||||
|
.addQueryParameter("query", query)
|
||||||
|
.addQueryParameter("limit", "9999")
|
||||||
|
.addQueryParameter("page", "1")
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
addQueryParameter("s", query)
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
}.build()
|
val mangas = response.parseAs<SearchDto>().data.map { it.toSManga() }
|
||||||
|
return MangasPage(mangas, hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// ============================== Details ======================================
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga) = "$baseUrl/$lang${manga.url}"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.selectFirst("h1")!!.text()
|
||||||
|
genre = document.select("a[href*=the-loai]").joinToString { it.attr("title") }
|
||||||
|
thumbnail_url = document.selectFirst("img.bg-background")?.absUrl("src")
|
||||||
|
document.selectFirst("h5")?.text()?.lowercase()?.let {
|
||||||
|
status = when {
|
||||||
|
it.equals("Đã hoàn thành", ignoreCase = true) -> SManga.COMPLETED
|
||||||
|
it.equals("Đang tiến hành", ignoreCase = true) -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUrlWithoutDomain(document.location())
|
||||||
|
url = url.removePrefix("/$lang")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ======================================
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/$lang${chapter.url}"
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val document = client.newCall(super.chapterListRequest(manga))
|
||||||
|
.execute().asJsoup()
|
||||||
|
val postId = document.findPostId()
|
||||||
|
return chapterListRequest(postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterListRequest(postId: String): Request {
|
||||||
|
val url = "$apiUrl/posts/$postId/chapters".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("language", lang)
|
||||||
|
.addQueryParameter("limit", "9999")
|
||||||
|
.addQueryParameter("page", "1")
|
||||||
|
.build()
|
||||||
|
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div[data-id] > div.card"
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
return response.parseAs<ChapterWrapper>().toSChapterList()
|
||||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
.sortedByDescending(SChapter::chapter_number)
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
val statusClassName = document.selectFirst("em.eflag.item-flag")!!.className()
|
|
||||||
|
|
||||||
title = document.selectFirst("span[itemprop=name]")!!.text()
|
|
||||||
author = document.select("div.attr-item b:contains(Tác giả) ~ span a, span[itemprop=author]").joinToString { it.text() }
|
|
||||||
description = document.selectFirst("div[itemprop=about]")?.text()
|
|
||||||
genre = document.select("ul.post-categories li a").joinToString { it.text() }
|
|
||||||
thumbnail_url = document.selectFirst("div.attr-cover img")?.absUrl("src")
|
|
||||||
status = when {
|
|
||||||
statusClassName.contains("flag-completed") -> SManga.COMPLETED
|
|
||||||
statusClassName.contains("flag-ongoing") -> SManga.ONGOING
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() = "#chaptersbox > div"
|
private fun Document.findPostId(): String {
|
||||||
|
val script = select("script").map(Element::data)
|
||||||
|
.first(CHAPTERS_POST_ID::containsMatchIn)
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
return CHAPTERS_POST_ID.find(script)?.groups?.get(1)?.value!!
|
||||||
element.selectFirst("a")!!.let {
|
|
||||||
setUrlWithoutDomain(it.attr("href"))
|
|
||||||
name = it.selectFirst("b")!!.text()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
date_upload = element.selectFirst("div.extra > i.ps-3")
|
override fun chapterListSelector() = throw UnsupportedOperationException()
|
||||||
?.text()
|
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
?.let { parseRelativeDate(it) }
|
|
||||||
?: 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document) =
|
// ============================== Pages ======================================
|
||||||
document.select("#viewer img").mapIndexed { i, it ->
|
|
||||||
Page(i, imageUrl = it.absUrl("src"))
|
override fun pageListRequest(chapter: SChapter) = GET(getChapterUrl(chapter), headers)
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val postId = document.findPostId()
|
||||||
|
val dto = client.newCall(chapterListRequest(postId))
|
||||||
|
.execute()
|
||||||
|
.parseAs<ChapterWrapper>()
|
||||||
|
|
||||||
|
val pathSegment = document.location()
|
||||||
|
.substringAfterLast("/")
|
||||||
|
.substringBeforeLast(".")
|
||||||
|
|
||||||
|
val page = dto.data.first { pathSegment.equals(it.slug, ignoreCase = true) }
|
||||||
|
|
||||||
|
return Jsoup.parseBodyFragment(page.content).select("img").mapIndexed { index, element ->
|
||||||
|
Page(index, imageUrl = element.absUrl("src"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val PREFIX_SLUG_SEARCH = "slug:"
|
internal const val PREFIX_SLUG_SEARCH = "slug:"
|
||||||
|
private val CHAPTERS_POST_ID = """(?:(?:postId|post_id).{3})(\d+)""".toRegex()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.vi.truyenhentai18
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchDto(
|
||||||
|
val data: List<MangaDto>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDto(
|
||||||
|
private val title: String,
|
||||||
|
private val slug: String,
|
||||||
|
) {
|
||||||
|
fun toSManga() = SManga.create().apply {
|
||||||
|
title = this@MangaDto.title
|
||||||
|
url = "/$slug.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterWrapper(
|
||||||
|
@SerialName("post_slug")
|
||||||
|
private val postSlug: String,
|
||||||
|
val data: List<ChapterDto>,
|
||||||
|
) {
|
||||||
|
fun toSChapterList() = data.map { it.toSChapter(postSlug) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterDto(
|
||||||
|
val slug: String,
|
||||||
|
@SerialName("chapter_number")
|
||||||
|
private val chapterNumber: Float,
|
||||||
|
@SerialName("created_at")
|
||||||
|
private val createdAt: String,
|
||||||
|
val content: String,
|
||||||
|
) {
|
||||||
|
fun toSChapter(postSlug: String) = SChapter.create().apply {
|
||||||
|
name = chapterNumber.toString()
|
||||||
|
chapter_number = chapterNumber
|
||||||
|
url = "/$postSlug/$slug.html"
|
||||||
|
date_upload = dateFormat.tryParse(createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||||
|
}
|
||||||
|
}
|
@ -13,10 +13,10 @@ class TruyenHentai18UrlActivity : Activity() {
|
|||||||
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
|
||||||
if (pathSegments != null && pathSegments.size > 0) {
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${TruyenHentai18.PREFIX_SLUG_SEARCH}${pathSegments[0]}")
|
putExtra("query", "${TruyenHentai18.PREFIX_SLUG_SEARCH}${pathSegments[1]}")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user