NhatTruyen update domain & fix missing chapter & fix search (#9175)

* bump version

* Update domain and Fix missing chapter

* fix build

* Use suggest

* Fix Search no results

* ¯\_(ツ)_/¯

* Change fetchChapterList to chaplistRequest
This commit is contained in:
are-are-are 2025-06-15 13:13:52 +07:00 committed by Draff
parent 205bf49af7
commit a9176c529b
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 71 additions and 133 deletions

View File

@ -2,8 +2,8 @@ ext {
extName = 'NhatTruyen'
extClass = '.NhatTruyen'
themePkg = 'wpcomics'
baseUrl = 'https://nhattruyenv.com'
overrideVersionCode = 20
baseUrl = 'https://nhattruyenqq.com'
overrideVersionCode = 21
isNsfw = false
}

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.vi.nhattruyen
import kotlinx.serialization.Serializable
@Serializable
class ChapterDTO(
val data: ArrayList<Data> = arrayListOf(),
)
@Serializable
class Data(
val chapter_name: String,
val chapter_slug: String,
val updated_at: String,
)

View File

@ -4,177 +4,99 @@ import eu.kanade.tachiyomi.multisrc.wpcomics.WPComics
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.MangasPage
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
class NhatTruyen : WPComics(
"NhatTruyen",
"https://nhattruyenv.com",
"https://nhattruyenqq.com",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
override val searchPath = "tim-truyen"
override val popularPath = "truyen-tranh-hot"
/**
* NetTruyen/NhatTruyen redirect back to catalog page if searching query is not found.
* That makes both sites always return un-relevant results when searching should return empty.
*/
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.toString().endsWith("/$searchPath")) {
return MangasPage(mangas = emptyList(), hasNextPage = false)
}
return super.searchMangaParse(response)
}
// Advanced search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isBlank()) {
val url = "$baseUrl/tim-truyen-nang-cao".toHttpUrl().newBuilder()
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
filters.forEach { filter ->
when (filter) {
is AdvancedGenreFilter -> {
filter.included.let { url.addQueryParameter("genres", it.joinToString(",")) }
filter.excluded.let { url.addQueryParameter("notgenres", it.joinToString(",")) }
}
is AdvancedStatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
is ChaptersNumFilter -> filter.toUriPart()?.let { url.addQueryParameter("minchapter", it) }
is GenderFilter -> filter.toUriPart()?.let { url.addQueryParameter("gender", it) }
is OrderFilter -> filter.toUriPart()?.let { url.addQueryParameter("sort", it) }
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
is OrderByFilter -> filter.toUriPart()?.let { url.addQueryParameter("sort", it) }
else -> {}
}
}
url.apply {
addQueryParameter(queryParam, query)
addQueryParameter("page", page.toString())
}
return GET(url.toString(), headers)
} else {
return super.searchMangaRequest(page, query, filters)
}
}
private class AdvancedGenre(val id: String, name: String) : Filter.TriState(name)
private class AdvancedGenreFilter(name: String, advancedGenres: List<AdvancedGenre>) : Filter.Group<AdvancedGenre>(
name,
advancedGenres.map { AdvancedGenre(it.id, it.name) },
) {
val included: List<String>
get() = state.filter { it.isIncluded() }.map { it.id }
val excluded: List<String>
get() = state.filter { it.isExcluded() }.map { it.id }
}
private var advancedGenresList: List<AdvancedGenre> = emptyList()
private var fetchAdvancedGenresAttempts: Int = 0
private fun fetchAdvancedGenres() {
if (fetchAdvancedGenresAttempts < 3 && advancedGenresList.isEmpty()) {
try {
advancedGenresList =
client.newCall(advancedGenresRequest()).execute()
.asJsoup()
.let(::parseAdvancedGenres)
} catch (_: Exception) {
} finally {
fetchAdvancedGenresAttempts++
}
}
}
private fun advancedGenresRequest() = GET("$baseUrl/tim-truyen-nang-cao", headers)
private fun parseAdvancedGenres(document: Document): List<AdvancedGenre> {
val items = document.select(".advsearch-form .genre-item")
return buildList(items.size) {
items.mapTo(this) {
AdvancedGenre(
it.select("span").attr("data-id"),
it.ownText(),
)
}
}
}
private class ChaptersNumFilter : UriPartFilter(
"Số lượng chapter",
listOf(
Pair("1", "> 0 chapter"),
Pair("50", ">= 50 chapter"),
Pair("100", ">= 100 chapter"),
Pair("200", ">= 200 chapter"),
Pair("300", ">= 300 chapter"),
Pair("400", ">= 400 chapter"),
Pair("500", ">= 500 chapter"),
),
)
private class AdvancedStatusFilter(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
private fun getAdvancedStatusList(): List<Pair<String?, String>> =
listOf(
Pair("-1", intl["STATUS_ALL"]),
Pair("1", intl["STATUS_ONGOING"]),
Pair("2", intl["STATUS_COMPLETED"]),
)
private class GenderFilter : UriPartFilter(
"Dành cho",
listOf(
Pair("-1", "Tất cả"),
Pair("1", "Con gái"),
Pair("2", "Con trai"),
),
)
private class OrderFilter : UriPartFilter(
private class OrderByFilter : UriPartFilter(
"Sắp xếp theo",
listOf(
Pair("0", "Chapter mới"),
Pair("0", "Ngày cập nhật"),
Pair("15", "Truyện mới"),
Pair("10", "Xem nhiều nhất"),
Pair("11", "Xem nhiều nhất tháng"),
Pair("12", "Xem nhiều nhất tuần"),
Pair("13", "Xem nhiều nhất hôm nay"),
Pair("20", "Theo dõi nhiều nhất"),
Pair("25", "Bình luận nhiều nhất"),
Pair("30", "Số chapter nhiều nhất"),
Pair("10", "Top all"),
Pair("11", "Top tháng"),
Pair("12", "Top tuần"),
Pair("13", "Top ngày"),
Pair("20", "Top theo dõi"),
Pair("25", "Bình luận"),
Pair("30", "Số chapter"),
),
)
override fun getFilterList(): FilterList {
launchIO { fetchAdvancedGenres() }
launchIO { fetchGenres() }
return FilterList(
Filter.Header("Filter cho hộp tìm kiếm"),
StatusFilter(intl["STATUS"], getStatusList()),
OrderByFilter(),
if (genreList.isEmpty()) {
Filter.Header(intl["GENRES_RESET"])
} else {
GenreFilter(intl["GENRE"], genreList)
},
Filter.Separator(),
Filter.Header("Tìm truyện nâng cao\n(Không sử dụng cùng với hộp tìm kiếm)"),
if (advancedGenresList.isEmpty()) {
Filter.Header(intl["GENRES_RESET"])
} else {
AdvancedGenreFilter(intl["GENRE"], advancedGenresList)
},
AdvancedStatusFilter(intl["STATUS"], getAdvancedStatusList()),
ChaptersNumFilter(),
GenderFilter(),
OrderFilter(),
)
}
override fun chapterListRequest(manga: SManga): Request {
val slug = manga.url.substringAfterLast("/") // slug
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("Comic/Services/ComicService.asmx/ChapterList")
.addQueryParameter("slug", slug)
.build()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val json = response.parseAs<ChapterDTO>()
val slug = response.request.url.queryParameter("slug")!!
val chapter = json.data.map {
SChapter.create().apply {
setUrlWithoutDomain("$baseUrl/truyen-tranh/$slug/${it.chapter_slug}")
name = it.chapter_name
date_upload = dateFormatChapter.tryParse(it.updated_at)
}
}
return chapter
}
private val dateFormatChapter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
}