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' extName = 'NhatTruyen'
extClass = '.NhatTruyen' extClass = '.NhatTruyen'
themePkg = 'wpcomics' themePkg = 'wpcomics'
baseUrl = 'https://nhattruyenv.com' baseUrl = 'https://nhattruyenqq.com'
overrideVersionCode = 20 overrideVersionCode = 21
isNsfw = false 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.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class NhatTruyen : WPComics( class NhatTruyen : WPComics(
"NhatTruyen", "NhatTruyen",
"https://nhattruyenv.com", "https://nhattruyenqq.com",
"vi", "vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()), dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null, gmtOffset = null,
) { ) {
override val searchPath = "tim-truyen" override val searchPath = "tim-truyen"
override val popularPath = "truyen-tranh-hot"
/** /**
* NetTruyen/NhatTruyen redirect back to catalog page if searching query is not found. * 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. * 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 { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isBlank()) { val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
val url = "$baseUrl/tim-truyen-nang-cao".toHttpUrl().newBuilder()
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is AdvancedGenreFilter -> { is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
filter.included.let { url.addQueryParameter("genres", it.joinToString(",")) } is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
filter.excluded.let { url.addQueryParameter("notgenres", it.joinToString(",")) } is OrderByFilter -> filter.toUriPart()?.let { url.addQueryParameter("sort", it) }
} else -> {}
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) }
else -> {}
}
}
url.apply {
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) url.apply {
addQueryParameter(queryParam, query)
private fun parseAdvancedGenres(document: Document): List<AdvancedGenre> { addQueryParameter("page", page.toString())
val items = document.select(".advsearch-form .genre-item")
return buildList(items.size) {
items.mapTo(this) {
AdvancedGenre(
it.select("span").attr("data-id"),
it.ownText(),
)
}
} }
return GET(url.toString(), headers)
} }
private class OrderByFilter : UriPartFilter(
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(
"Sắp xếp theo", "Sắp xếp theo",
listOf( listOf(
Pair("0", "Chapter mới"), Pair("0", "Ngày cập nhật"),
Pair("15", "Truyện mới"), Pair("15", "Truyện mới"),
Pair("10", "Xem nhiều nhất"), Pair("10", "Top all"),
Pair("11", "Xem nhiều nhất tháng"), Pair("11", "Top tháng"),
Pair("12", "Xem nhiều nhất tuần"), Pair("12", "Top tuần"),
Pair("13", "Xem nhiều nhất hôm nay"), Pair("13", "Top ngày"),
Pair("20", "Theo dõi nhiều nhất"), Pair("20", "Top theo dõi"),
Pair("25", "Bình luận nhiều nhất"), Pair("25", "Bình luận"),
Pair("30", "Số chapter nhiều nhất"), Pair("30", "Số chapter"),
), ),
) )
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
launchIO { fetchAdvancedGenres() }
launchIO { fetchGenres() } launchIO { fetchGenres() }
return FilterList( return FilterList(
Filter.Header("Filter cho hộp tìm kiếm"),
StatusFilter(intl["STATUS"], getStatusList()), StatusFilter(intl["STATUS"], getStatusList()),
OrderByFilter(),
if (genreList.isEmpty()) { if (genreList.isEmpty()) {
Filter.Header(intl["GENRES_RESET"]) Filter.Header(intl["GENRES_RESET"])
} else { } else {
GenreFilter(intl["GENRE"], genreList) 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)
} }