From 2c1789d7785a4f5c1453c2413cdf6c5ed11aedd7 Mon Sep 17 00:00:00 2001
From: beerpsi <92439990+beerpiss@users.noreply.github.com>
Date: Sun, 28 Jan 2024 18:39:57 +0700
Subject: [PATCH] TruyenGiHot: Update base URL and a lot of fixes (#747)
---
src/vi/truyengihot/AndroidManifest.xml | 2 +-
src/vi/truyengihot/build.gradle | 2 +-
.../extension/vi/truyengihot/TruyenGiHot.kt | 526 ++++--------------
.../vi/truyengihot/TruyenGiHotFilters.kt | 137 +++++
.../vi/truyengihot/TruyenGiHotUtils.kt | 69 +++
5 files changed, 308 insertions(+), 428 deletions(-)
create mode 100644 src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotFilters.kt
create mode 100644 src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotUtils.kt
diff --git a/src/vi/truyengihot/AndroidManifest.xml b/src/vi/truyengihot/AndroidManifest.xml
index 58d189dcf..b816a4e3b 100644
--- a/src/vi/truyengihot/AndroidManifest.xml
+++ b/src/vi/truyengihot/AndroidManifest.xml
@@ -11,7 +11,7 @@
-
diff --git a/src/vi/truyengihot/build.gradle b/src/vi/truyengihot/build.gradle
index 9220e4c6f..a42783509 100644
--- a/src/vi/truyengihot/build.gradle
+++ b/src/vi/truyengihot/build.gradle
@@ -1,7 +1,7 @@
ext {
extName = 'TruyenGiHot'
extClass = '.TruyenGiHot'
- extVersionCode = 3
+ extVersionCode = 4
isNsfw = true
}
diff --git a/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHot.kt b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHot.kt
index 1314d2a54..fdb35e096 100644
--- a/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHot.kt
+++ b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHot.kt
@@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.extension.vi.truyengihot
+import android.util.Log
+import eu.kanade.tachiyomi.extension.vi.truyengihot.TruyenGiHotUtils.imgAttr
+import eu.kanade.tachiyomi.extension.vi.truyengihot.TruyenGiHotUtils.textWithNewlines
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@@ -10,38 +13,39 @@ 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.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
-import okhttp3.Headers
-import okhttp3.HttpUrl
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 rx.Observable
+import rx.Single
+import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
-import java.util.Calendar
class TruyenGiHot : ParsedHttpSource() {
override val name: String = "TruyenGiHot"
- override val baseUrl: String = "https://truyengihotne.com"
+ override val baseUrl: String = "https://truyengihotqua.com"
override val lang: String = "vi"
- override val supportsLatest: Boolean = true
+ override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
- override fun headersBuilder(): Headers.Builder =
- super.headersBuilder().add("Referer", "$baseUrl/")
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
@@ -58,6 +62,7 @@ class TruyenGiHot : ParsedHttpSource() {
getSortItems(),
Filter.Sort.Selection(2, false),
),
+ CategoryFilter(0),
),
)
@@ -77,6 +82,7 @@ class TruyenGiHot : ParsedHttpSource() {
getSortItems(),
Filter.Sort.Selection(0, false),
),
+ CategoryFilter(0),
),
)
@@ -114,52 +120,36 @@ class TruyenGiHot : ParsedHttpSource() {
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val url =
- "$baseUrl/tim-kiem-nang-cao.html?listType=table&page=$page".toHttpUrl().newBuilder()
- .apply {
- val genres = mutableListOf()
- val genresEx = mutableListOf()
+ runCatching { fetchFilterOptions() }
+ val url =
+ "$baseUrl/danh-sach-truyen.html?listType=thumb&page=$page".toHttpUrl().newBuilder()
+ .apply {
addQueryParameter("text_add", query)
- (if (filters.isEmpty()) getFilterList() else filters).forEach {
- when (it) {
- is UriFilter -> it.addToUri(this)
- is GenreFilter -> it.state.forEach { genre ->
- when (genre.state) {
- Filter.TriState.STATE_INCLUDE -> genres.add(genre.id)
- Filter.TriState.STATE_EXCLUDE -> genresEx.add(genre.id)
- else -> {}
- }
- }
- else -> {}
- }
- }
-
- addQueryParameter("tag_add", genres.joinToString(","))
- addQueryParameter("tag_remove", genresEx.joinToString(","))
- }.build().toString()
+ (if (filters.isEmpty()) getFilterList() else filters)
+ .filterIsInstance()
+ .forEach { it.addToUri(this) }
+ }.build()
return GET(url, headers)
}
- override fun searchMangaSelector(): String = "ul.cw-list li"
+ override fun searchMangaSelector(): String = "ul.contentList li"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
val anchor = element.select("span.title a")
setUrlWithoutDomain(anchor.attr("href"))
title = anchor.text()
- thumbnail_url = baseUrl + element.select("span.thumb").attr("style")
- .substringAfter("url('")
- .substringBefore("')")
+ thumbnail_url = element.selectFirst("span.thumb img")?.imgAttr()
}
- override fun searchMangaNextPageSelector(): String = "li.page-next a:not(.disabled)"
+ override fun searchMangaNextPageSelector(): String = "li.page-next"
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select(".cover-title").text()
author = document.select("p.cover-artist:contains(Tác giả) a").joinToString { it.text() }
genre = document.select("a.manga-tags").joinToString { it.text().removePrefix("#") }
- thumbnail_url = document.select("div.cover-image img").attr("abs:src")
+ thumbnail_url = document.selectFirst("div.cover-image img")?.imgAttr()
val tags = document.select("img.top-tags.top-tags-full").map {
it.attr("src").substringAfterLast("/").substringBefore(".png")
@@ -171,41 +161,34 @@ class TruyenGiHot : ParsedHttpSource() {
else -> SManga.UNKNOWN
}
- description = document.select("div.product-synopsis-content").run {
+ description = document.select("div.content div.textArea").run {
select("p").first()?.prepend("|truyengihay-split|")
- text().substringAfter("|truyengihay-split|").substringBefore(" Xem thêm")
+ textWithNewlines().substringAfter("|truyengihay-split|").substringBefore(" Xem thêm")
}
}
- override fun chapterListSelector(): String = "ul.episode-list li a"
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+ val contentType = document.select("ul.breadcrumb li")[1].text()
+
+ // Because they show up even with a manga filter in place
+ if (contentType == "Novel" || contentType == "Anime") {
+ return emptyList()
+ }
+
+ return document.select(chapterListSelector()).map {
+ chapterFromElement(it)
+ }
+ }
+
+ override fun chapterListSelector(): String = "ul#episode_list li a"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
val infoBlock = element.selectFirst("span.info")!!
name = infoBlock.select("span.no").text()
- date_upload = parseChapterDate(infoBlock.select("span.date").text())
- }
-
- private fun parseChapterDate(date: String): Long {
- val trimmedDate = date.substringBefore(" trước").split(" ")
-
- val calendar = Calendar.getInstance().apply {
- val amount = -trimmedDate[0].toInt()
- val field = when (trimmedDate[1]) {
- "giây" -> Calendar.SECOND
- "phút" -> Calendar.MINUTE
- "giờ" -> Calendar.HOUR_OF_DAY
- "ngày" -> Calendar.DAY_OF_MONTH
- "tuần" -> Calendar.WEEK_OF_MONTH
- "tháng" -> Calendar.MONTH
- "năm" -> Calendar.YEAR
- else -> Calendar.SECOND
- }
- add(field, amount)
- }
-
- return calendar.timeInMillis
+ date_upload = TruyenGiHotUtils.parseChapterDate(infoBlock.select("span.date").text())
}
override fun pageListParse(document: Document): List {
@@ -229,7 +212,7 @@ class TruyenGiHot : ParsedHttpSource() {
val formBody = FormBody.Builder()
.add("token", token)
- .add("chapter_id", chapterInfo["cid"]!!)
+ .add("chapter_id", chapterInfo["c_id"]!!)
.add("m_slug", chapterInfo["mangaSLUG"]!!)
.add("m_id", chapterInfo["mangaID"]!!)
.add("chapter", chapterInfo["chapter"]!!)
@@ -247,386 +230,77 @@ class TruyenGiHot : ParsedHttpSource() {
throw Exception("Truyện đã bị khoá!")
}
- return Jsoup.parseBodyFragment(pageHtml, baseUrl).select("img").mapIndexed { idx, it ->
- Page(idx, imageUrl = it.attr("abs:src"))
+ return Jsoup.parseBodyFragment(pageHtml, baseUrl).select("img:not([src$=wattermark.png])").mapIndexed { idx, it ->
+ Page(idx, imageUrl = it.imgAttr())
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
- override fun getFilterList(): FilterList = FilterList(
- SearchTypeFilter(),
- CategoryFilter(),
- PublicationTypeFilter(),
- CountryFilter(),
- StatusFilter(),
- ScanlatorFilter(),
- SortFilter(getSortItems()),
- GenreFilter(),
- )
+ override fun getFilterList(): FilterList {
+ val filters = mutableListOf>(
+ CategoryFilter(),
+ PublicationTypeFilter(),
+ FormatTypeFilter(),
+ MagazineFilter(),
+ ExplicitFilter(),
+ StatusFilter(),
+ ).also {
+ if ((tags.isEmpty() && themes.isEmpty() && scanlators.isEmpty()) || fetchFiltersFailed) {
+ it.add(0, Filter.Header("Nhấn 'Đặt lại' để hiện các bộ lọc"))
+ it.add(1, Filter.Separator())
+ }
- interface UriFilter {
- fun addToUri(builder: HttpUrl.Builder)
- }
+ if (scanlators.isNotEmpty()) {
+ it.add(ScanlatorFilter(scanlators.toTypedArray()))
+ }
- open class UriPartFilter(
- name: String,
- private val query: String,
- private val vals: Array>,
- state: Int = 0,
- ) : UriFilter, Filter.Select(name, vals.map { it.first }.toTypedArray(), state) {
- override fun addToUri(builder: HttpUrl.Builder) {
- builder.addQueryParameter(query, vals[state].second)
+ if (tags.isNotEmpty()) {
+ it.add(TagFilter(tags))
+ }
+
+ if (themes.isNotEmpty()) {
+ it.add(ThemesFilter(themes))
+ }
+
+ it.add(SortFilter(getSortItems()))
}
+
+ return FilterList(filters)
}
- private class SearchTypeFilter : UriPartFilter(
- "Tìm từ khoá theo",
- "text_type",
- arrayOf(
- Pair("Tên truyện", "name"),
- Pair("Tác giả", "authors"),
- ),
- )
+ private var tags: List = emptyList()
- private class CategoryFilter : UriPartFilter(
- "Phân loại",
- "type_add",
- arrayOf(
- Pair("Tất cả", ""),
- Pair("Truyện 18+", "truyen-tranh"),
- Pair("Ngôn tình", "ngon-tinh"),
- ),
- )
+ private var themes: List = emptyList()
- private class PublicationTypeFilter : UriPartFilter(
- "Thể loại",
- "genre_add",
- arrayOf(
- Pair("Tất cả", ""),
- Pair("Manga", "manga"),
- Pair("Manhua", "manhua"),
- Pair("Manhwa", "manhwa"),
- Pair("Tự sáng tác", "tu-sang-tac"),
- Pair("Khác", "khac"),
- ),
- )
+ private var scanlators: List> = emptyList()
- private class CountryFilter : UriPartFilter(
- "Quốc gia",
- "country_add",
- arrayOf(
- Pair("Tất cả", ""),
- Pair("Âu Mỹ", "au-my"),
- Pair("Hàn Quốc", "han-quoc"),
- Pair("Khác", "khac"),
- Pair("Nhật Bản", "nhat-ban"),
- Pair("Trung Quốc", "trung-quoc"),
- Pair("Việt Nam", "viet-nam"),
- ),
- )
+ private var fetchFiltersFailed = false
- private class StatusFilter : UriPartFilter(
- "Trạng thái",
- "status_add",
- arrayOf(
- Pair("Tất cả", "0"),
- Pair("Full", "1"),
- Pair("Ongoing", "2"),
- Pair("Drop", "3"),
- ),
- )
+ private var fetchFiltersAttempts = 0
- private class SortFilter(
- private val vals: Array>,
- state: Selection = Selection(2, false),
- ) : UriFilter,
- Filter.Sort("Sắp xếp", vals.map { it.first }.toTypedArray(), state) {
- override fun addToUri(builder: HttpUrl.Builder) {
- builder.addQueryParameter("order_add", vals[state?.index ?: 2].second)
- builder.addQueryParameter(
- "order_by_add",
- if (state?.ascending == true) "ASC" else "DESC",
- )
+ private fun fetchFilterOptions() {
+ if (fetchFiltersAttempts > 3 || (fetchFiltersAttempts > 0 && !fetchFiltersFailed)) {
+ return
}
+
+ Single.fromCallable {
+ val document = client.newCall(GET("$baseUrl/danh-sach-truyen.html", headers)).execute().asJsoup()
+
+ val result = runCatching {
+ tags = TruyenGiHotUtils.parseThemes(document.selectFirst("#contentTag")!!)
+ themes = TruyenGiHotUtils.parseThemes(document.selectFirst("#contentTheme")!!)
+ scanlators = TruyenGiHotUtils.parseOptions(document.selectFirst("#contentGroup")!!)
+ }
+ .onFailure {
+ Log.e("TruyenGiHot", "Could not fetch filtering options", it)
+ }
+
+ fetchFiltersFailed = result.isFailure
+ fetchFiltersAttempts++
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.io())
+ .subscribe()
}
-
- private fun getSortItems(): Array> = arrayOf(
- Pair("Mới cập nhật", "last_update"),
- Pair("Lượt xem", "views"),
- Pair("Hot", "total_vote"),
- Pair("Vote", "count_vote"),
- Pair("Tên A-Z", "name"),
- )
-
- private class Genre(name: String, val id: String) : Filter.TriState(name)
-
- // console.log([...document.querySelectorAll(".wrapper-search-tag .search-content span")].map(e => `Genre("${e.innerText.trim()}", "${e.dataset.val}")`).join(",\n"))
- private class GenreFilter : Filter.Group(
- "Chủ đề",
- listOf(
- Genre("16+", "16"),
- Genre("18+", "18"),
- Genre("1Vs1", "1vs1"),
- Genre("3d", "3d"),
- Genre("3some", "3some"),
- Genre("Ác nữ", "ac-nu"),
- Genre("Ác Quỷ", "ac-quy"),
- Genre("Action", "action"),
- Genre("Adult", "adult"),
- Genre("Adventure", "adventure"),
- Genre("ai cập", "ai-cap"),
- Genre("Âm Nhạc", "am-nhac"),
- Genre("Anh chị em", "anh-chi-em"),
- Genre("anh chị em kế", "anh-chi-em-ke"),
- Genre("anh hùng", "anh-hung"),
- Genre("Anime", "anime"),
- Genre("artist cg", "artist-cg"),
- Genre("Âu Cổ", "au-co"),
- Genre("Bách Hợp", "bach-hop"),
- Genre("bad boy", "bad-boy"),
- Genre("bạn thân", "ban-than"),
- Genre("Bạo Lực", "bao-luc"),
- Genre("Bdsm", "bdsm"),
- Genre("BE", "be"),
- Genre("Bí Ẩn", "bi-an"),
- Genre("Bi kịch", "bi-kich"),
- Genre("bị vứt bỏ", "bi-vut-bo"),
- Genre("big breast", "big-breast"),
- Genre("BL/Bách hợp", "bl-bach-hop"),
- Genre("blowjobs", "blowjobs"),
- Genre("bỏ trốn", "bo-tron"),
- Genre("cái chết", "cai-chet"),
- Genre("Cận đại", "can-dai"),
- Genre("Cấu Huyết", "cau-huyet"),
- Genre("Châu Âu", "chau-au"),
- Genre("che", "che"),
- Genre("Chiến Tranh", "chien-tranh"),
- Genre("Chuyển Sinh", "chuyen-sinh"),
- Genre("Chuyển Thế", "chuyen-the"),
- Genre("Cổ Đại", "co-dai"),
- Genre("Cổ Trang", "co-trang"),
- Genre("con gái nô", "con-gai-no"),
- Genre("con ngoài dã thú", "con-ngoai-da-thu"),
- Genre("công sở", "cong-so"),
- Genre("Cung Đấu", "cung-dau"),
- Genre("đẹp trai Nam chính", "dep-trai-nam-chinh"),
- Genre("Dị Giới", "di-gioi"),
- Genre("Dị Năng", "di-nang"),
- Genre("Điền Văn", "dien-van"),
- Genre("dl site", "dl-site"),
- Genre("Đô Thị", "do-thi"),
- Genre("Đoản Văn", "doan-van"),
- Genre("độc ác Nữ chính", "doc-ac-nu-chinh"),
- Genre("Drama", "drama"),
- Genre("Được nhận nuôi", "duoc-nhan-nuoi"),
- Genre("Ecchi", "ecchi"),
- Genre("Fantasy", "fantasy"),
- Genre("Game", "game"),
- Genre("Gây cấn", "gay-can"),
- Genre("Gia Đình", "gia-dinh"),
- Genre("giả gái/trai", "gia-gai-trai"),
- Genre("Giai cấp quý tộc", "giai-cap-quy-toc"),
- Genre("giam cầm", "giam-cam"),
- Genre("giang hồ", "giang-ho"),
- Genre("Hài Hước", "hai-huoc"),
- Genre("hàng khủng", "hang-khung"),
- Genre("hàng xóm", "hang-xom"),
- Genre("Hành Động", "hanh-dong"),
- Genre("Harem", "harem"),
- Genre("HE", "he"),
- Genre("Hệ Thống", "he-thong"),
- Genre("Hentai", "hentai"),
- Genre("Hiện Đại", "hien-dai"),
- Genre("Hiểu lầm", "hieu-lam"),
- Genre("Hoán Đổi", "hoan-doi"),
- Genre("Hoàng gia", "hoang-gia"),
- Genre("Hoạt Hình", "hoat-hinh"),
- Genre("Học Đường", "hoc-duong"),
- Genre("học sinh", "hoc-sinh"),
- Genre("hối hận", "hoi-han"),
- Genre("Hồi hộp", "hoi-hop"),
- Genre("Huyền Ảo", "huyen-ao"),
- Genre("Ít che", "it-che"),
- Genre("kaka*page", "kaka-page"),
- Genre("khổ dâm", "kho-dam"),
- Genre("Khoa Học", "khoa-hoc"),
- Genre("không che", "khong-che"),
- Genre("Không Màu", "khong-mau"),
- Genre("Kiếm Hiệp", "kiem-hiep"),
- Genre("Kinh Dị", "kinh-di"),
- Genre("Lãng mạn", "lang-man"),
- Genre("lezh*n", "lezh-n"),
- Genre("Lịch Sử", "lich-su"),
- Genre("Light Novel", "light-novel"),
- Genre("Live action", "live-action"),
- Genre("loạn luân", "loan-luan"),
- Genre("Loli", "loli"),
- Genre("ma", "ma"),
- Genre("Ma Cà Rồng", "ma-ca-rong"),
- Genre("mang thai", "mang-thai"),
- Genre("Manga", "manga"),
- Genre("Manhua", "manhua"),
- Genre("Manhwa", "manhwa"),
- Genre("Mạt Thế", "mat-the"),
- Genre("mẹ kế", "me-ke"),
- Genre("Mô tả đế chế", "mo-ta-de-che"),
- Genre("mystery", "mystery"),
- Genre("nam duy nhất", "nam-duy-nhat"),
- Genre("nav*r", "nav-r"),
- Genre("nét vẽ Đẹp", "net-ve-dep"),
- Genre("Netflix", "netflix"),
- Genre("Ngây thơ", "ngay-tho"),
- Genre("ngoại tình", "ngoai-tinh"),
- Genre("Ngôn Tình", "ngon-tinh"),
- Genre("Ngược", "nguoc"),
- Genre("người hầu", "nguoi-hau"),
- Genre("nhân thú", "nhan-thu"),
- Genre("Nhân vật chính", "nhan-vat-chinh"),
- Genre("nhân vật game", "nhan-vat-game"),
- Genre("Ninja", "ninja"),
- Genre("nô lệ", "no-le"),
- Genre("ntr", "ntr"),
- Genre("Nữ Cường", "nu-cuong"),
- Genre("nữ duy nhất", "nu-duy-nhat"),
- Genre("Nữ Phụ", "nu-phu"),
- Genre("Oan gia", "oan-gia"),
- Genre("OE", "oe"),
- Genre("old man", "old-man"),
- Genre("oneshot", "oneshot"),
- Genre("otome game", "otome-game"),
- Genre("otp", "otp"),
- Genre("phản diện", "phan-dien"),
- Genre("Phép Thuật", "phep-thuat"),
- Genre("Phiêu Lưu", "phieu-luu"),
- Genre("Phim Bộ", "phim-bo"),
- Genre("Phim Chiếu Rạp", "phim-chieu-rap"),
- Genre("Phim Lẻ", "phim-le"),
- Genre("prologue", "prologue"),
- Genre("psychological", "psychological"),
- Genre("quái vật", "quai-vat"),
- Genre("Quân Sự", "quan-su"),
- Genre("Quý tộc", "quy-toc"),
- Genre("rape", "rape"),
- Genre("Sắc", "sac"),
- Genre("Sạch", "sach"),
- Genre("SE", "se"),
- Genre("seinen", "seinen"),
- Genre("sex toy", "sex-toy"),
- Genre("shoujo", "shoujo"),
- Genre("Shoujo Ai", "shoujo-ai"),
- Genre("Siêu Năng Lực", "sieu-nang-luc"),
- Genre("slice of life", "slice-of-life"),
- Genre("Smut", "smut"),
- Genre("Sở thích tra tấn", "so-thich-tra-tan"),
- Genre("Sủng", "sung"),
- Genre("supernatural", "supernatural"),
- Genre("tái sinh", "tai-sinh"),
- Genre("Tâm Lý", "tam-ly"),
- Genre("thẩm du", "tham-du"),
- Genre("Thám Hiểm", "tham-hiem"),
- Genre("Thần Thoại", "than-thoai"),
- Genre("thánh nữ", "thanh-nu"),
- Genre("thanh xuân vườn trường", "thanh-xuan-vuon-truong"),
- Genre("thầy/cô giáo", "thay-co-giao"),
- Genre("thay Đổi cốt truyện", "thay-doi-cot-truyen"),
- Genre("thay Đổi giới tính", "thay-doi-gioi-tinh"),
- Genre("Thể Thao", "the-thao"),
- Genre("thuần hóa", "thuan-hoa"),
- Genre("Tiên Hiệp", "tien-hiep"),
- Genre("Tiểu Thuyết", "tieu-thuyet"),
- Genre("Tình Cảm", "tinh-cam"),
- Genre("Tình Tay Ba", "tinh-tay-ba"),
- Genre("Tổng Tài", "tong-tai"),
- Genre("trà xanh", "tra-xanh"),
- Genre("Trailer", "trailer"),
- Genre("Trinh Thám", "trinh-tham"),
- Genre("Trọng Sinh", "trong-sinh"),
- Genre("Truyện Màu", "truyen-mau"),
- Genre("tsundere", "tsundere"),
- Genre("Tự Sáng Tác", "tu-sang-tac"),
- Genre("tưởng tượng", "tuong-tuong"),
- Genre("tuyển tập", "tuyen-tap"),
- Genre("vị hôn thê", "vi-hon-the"),
- Genre("Việt Nam", "viet-nam"),
- Genre("Võ Thuật", "vo-thuat"),
- Genre("Vũ Trụ", "vu-tru"),
- Genre("Webtoon", "webtoon"),
- Genre("xúc tua", "xuc-tua"),
- Genre("Xuyên Không", "xuyen-khong"),
- Genre("Xuyên không/Trọng sinh", "xuyen-khong-trong-sinh"),
- Genre("Yandere", "yandere"),
- Genre("Yuri", "yuri"),
- ),
- )
-
- // console.log([...document.querySelectorAll(".wrapper-search-group .search-content span")].map(e => `Pair("${e.innerText.trim()}", "${e.dataset.val}")`).join(",\n"))
- private class ScanlatorFilter : UriPartFilter(
- "Nhóm dịch",
- "group_add",
- arrayOf(
- Pair("Tất cả", "0"),
- Pair("Aling - Tiểu Thuyết", "383"),
- Pair("Angela Diệp Lạc", "361"),
- Pair("AUTHOR TIỂU MÂY", "362"),
- Pair("Boom novel", "403"),
- Pair("Cà chua Team", "421"),
- Pair("Camellia", "300"),
- Pair("Cậu Muốn Review Gì Nào?", "342"),
- Pair("Chloe's Library", "392"),
- Pair("Delion", "376"),
- Pair("Ecchi Land", "26"),
- Pair("Fluer", "396"),
- Pair("Gangster", "327"),
- Pair("Hien serena", "330"),
- Pair("Hoạ Y", "417"),
- Pair("Khu Vườn Bí Mật Của Rosaria", "401"),
- Pair("Laziel", "377"),
- Pair("Lazy Bee", "420"),
- Pair("Lil Pan", "334"),
- Pair("Lindy", "399"),
- Pair("Lọ Lem Hangul", "6"),
- Pair("Lycoris Radiata - Tiểu Hoa", "407"),
- Pair("MARY CƠM TRÓ", "423"),
- Pair("Mary Hạ Lục", "38"),
- Pair("Mây", "349"),
- Pair("Mảy Dus GL", "425"),
- Pair("Mảy Lành Mạnh", "424"),
- Pair("Mây Mây", "409"),
- Pair("meoluoihamchoi", "385"),
- Pair("Miêu Tặc", "343"),
- Pair("Mộc Trà", "306"),
- Pair("Một Chiếc Mèo Màu Đen", "390"),
- Pair("Nam Tử Sa Page", "20"),
- Pair("Nô Vồ", "393"),
- Pair("NỒI CƠM TRÓ", "382"),
- Pair("NỒI CƠM TRÓ 18+", "426"),
- Pair("Ổ Của Sien", "321"),
- Pair("Reviewer", "369"),
- Pair("Reviews", "419"),
- Pair("RINNIE", "341"),
- Pair("Rose The One", "337"),
- Pair("Roselight Team", "402"),
- Pair("Song Tử", "305"),
- Pair("The Present Translator", "404"),
- Pair("Thiên Mộc Thất Tú", "304"),
- Pair("Thư Viện Latsya", "370"),
- Pair("Tiệm Kẹo Dẻo Ngòn Ngon", "418"),
- Pair("Tiểu Miêu Ngốc", "395"),
- Pair("Tiểu Thuyết Nhà Mây", "347"),
- Pair("Tiểu Vũ", "360"),
- Pair("TIỂU VY", "388"),
- Pair("tieu.yet", "355"),
- Pair("Trà Và Bánh", "40"),
- Pair("Traham", "319"),
- Pair("Truyện dịch Team Behira", "410"),
- Pair("Truyện Tổng Hợp", "23"),
- Pair("Windyzzz", "379"),
- Pair("Xóm Bán Hoa", "364"),
- Pair("Yu", "406"),
- Pair("Đào Lý Tửu", "345"),
- Pair("Đảo San Hô", "397"),
- Pair("Điền Thất", "373"),
- ),
- )
}
diff --git a/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotFilters.kt b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotFilters.kt
new file mode 100644
index 000000000..433330d90
--- /dev/null
+++ b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotFilters.kt
@@ -0,0 +1,137 @@
+package eu.kanade.tachiyomi.extension.vi.truyengihot
+
+import eu.kanade.tachiyomi.source.model.Filter
+import okhttp3.HttpUrl
+
+interface UriFilter {
+ fun addToUri(builder: HttpUrl.Builder)
+}
+
+open class UriPartFilter(
+ name: String,
+ private val query: String,
+ private val vals: Array>,
+ state: Int = 0,
+) : UriFilter, Filter.Select(name, vals.map { it.first }.toTypedArray(), state) {
+ override fun addToUri(builder: HttpUrl.Builder) {
+ builder.addQueryParameter(query, vals[state].second)
+ }
+}
+
+internal class SortFilter(
+ private val vals: Array>,
+ state: Selection = Selection(2, false),
+) : UriFilter,
+ Filter.Sort("Sắp xếp", vals.map { it.first }.toTypedArray(), state) {
+ override fun addToUri(builder: HttpUrl.Builder) {
+ builder.addQueryParameter("order_add", vals[state?.index ?: 2].second)
+ builder.addQueryParameter(
+ "order_by_add",
+ if (state?.ascending == true) "ASC" else "DESC",
+ )
+ }
+}
+
+internal class Genre(name: String, val id: String) : Filter.TriState(name)
+
+internal open class GenreGroup(name: String, private val key: String, state: List) : Filter.Group(name, state), UriFilter {
+ override fun addToUri(builder: HttpUrl.Builder) {
+ val incl = mutableListOf()
+ val excl = mutableListOf()
+
+ state.forEach {
+ when (it.state) {
+ TriState.STATE_INCLUDE -> incl.add(it.id)
+ TriState.STATE_EXCLUDE -> excl.add(it.id)
+ else -> {}
+ }
+ }
+
+ builder.addQueryParameter("${key}_add", incl.joinToString(","))
+ builder.addQueryParameter("${key}_remove", excl.joinToString(","))
+ }
+}
+
+internal class CategoryFilter(state: Int = 0) : UriPartFilter(
+ "Phân loại",
+ "type_add",
+ arrayOf(
+ // The site also has novels and anime.
+ Pair("Tất cả", "manga"),
+ Pair("Truyện 18+", "audult"),
+ Pair("Ngôn tình", "noaudult"),
+ ),
+ state,
+)
+
+internal class PublicationTypeFilter : UriPartFilter(
+ "Thể loại",
+ "genre_add",
+ arrayOf(
+ Pair("Tất cả", "0"),
+ Pair("Manga", "29"),
+ Pair("Manhua", "30"),
+ Pair("Manhwa", "31"),
+ Pair("Tự sáng tác", "206"),
+ ),
+)
+
+internal class FormatTypeFilter : UriPartFilter(
+ "Format",
+ "format_add",
+ arrayOf(
+ Pair("Tất cả", "0"),
+ Pair("R15+", "307"),
+ Pair("R16+", "56"),
+ Pair("R18+", "128"),
+ Pair("R21+", "302"),
+ ),
+)
+
+internal class MagazineFilter : UriPartFilter(
+ "Magazines",
+ "magazine_add",
+ arrayOf(
+ Pair("Tất cả", "0"),
+ Pair("DL Site", "215"),
+ Pair("kaka*page", "217"),
+ Pair("lezh*n", "216"),
+ Pair("nav*r", "218"),
+ ),
+)
+
+internal class StatusFilter : UriPartFilter(
+ "Trạng thái",
+ "status_add",
+ arrayOf(
+ Pair("Tất cả", "0"),
+ Pair("Full", "1"),
+ Pair("Ongoing", "2"),
+ Pair("Drop", "3"),
+ ),
+)
+
+internal class ExplicitFilter : UriPartFilter(
+ "Explicit",
+ "explicit_add",
+ arrayOf(
+ Pair("Tất cả", "0"),
+ Pair("Ecchi", "21"),
+ Pair("Hentai", "73"),
+ Pair("Oneshot", "230"),
+ ),
+)
+
+internal class ScanlatorFilter(vals: Array>) : UriPartFilter("Nhóm dịch", "group_add", vals)
+
+internal class TagFilter(state: List) : GenreGroup("Tags", "tag", state)
+
+internal class ThemesFilter(state: List) : GenreGroup("Themes", "themes", state)
+
+internal fun getSortItems(): Array> = arrayOf(
+ Pair("Mới cập nhật", "last_update"),
+ Pair("Lượt xem", "views"),
+ Pair("Rating", "rating"),
+ Pair("Vote", "vote_c"),
+ Pair("Tên A-Z", "name"),
+)
diff --git a/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotUtils.kt b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotUtils.kt
new file mode 100644
index 000000000..f86c27b25
--- /dev/null
+++ b/src/vi/truyengihot/src/eu/kanade/tachiyomi/extension/vi/truyengihot/TruyenGiHotUtils.kt
@@ -0,0 +1,69 @@
+package eu.kanade.tachiyomi.extension.vi.truyengihot
+
+import org.jsoup.nodes.Element
+import org.jsoup.select.Elements
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+object TruyenGiHotUtils {
+ private val dateFormat: SimpleDateFormat by lazy {
+ SimpleDateFormat("dd.M.yy", Locale.US).apply {
+ timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
+ }
+ }
+
+ internal fun parseChapterDate(date: String): Long {
+ val trimmedDate = date.split(" ")
+
+ if (trimmedDate.size < 2) {
+ return runCatching {
+ dateFormat.parse(date)!!.time
+ }.getOrDefault(0L)
+ }
+
+ val calendar = Calendar.getInstance().apply {
+ val amount = -trimmedDate[0].toInt()
+ val field = when (trimmedDate[1]) {
+ "giây" -> Calendar.SECOND
+ "phút" -> Calendar.MINUTE
+ "giờ" -> Calendar.HOUR_OF_DAY
+ "ngày" -> Calendar.DAY_OF_MONTH
+ "tuần" -> Calendar.WEEK_OF_MONTH
+ "tháng" -> Calendar.MONTH
+ "năm" -> Calendar.YEAR
+ else -> Calendar.SECOND
+ }
+
+ add(field, amount)
+ }
+
+ return calendar.timeInMillis
+ }
+
+ internal fun parseThemes(element: Element): List {
+ return element.select("span[data-val]").map {
+ Genre(it.text(), it.attr("data-val"))
+ }
+ }
+
+ internal fun parseOptions(element: Element): List> {
+ return element.select("span[data-val]").map {
+ Pair(it.text(), it.attr("data-val"))
+ }
+ }
+
+ internal fun Element.imgAttr() = when {
+ hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
+ hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
+ hasAttr("data-src") -> absUrl("data-src")
+ hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
+ else -> absUrl("src")
+ }
+
+ internal fun Elements.textWithNewlines() = run {
+ select("p, br").prepend("\\n")
+ text().replace("\\n", "\n").replace("\n ", "\n")
+ }
+}