diff --git a/src/vi/truyenqq/build.gradle b/src/vi/truyenqq/build.gradle index ea04fb879..dfa072bf6 100644 --- a/src/vi/truyenqq/build.gradle +++ b/src/vi/truyenqq/build.gradle @@ -5,7 +5,7 @@ ext { extName = 'TruyenQQ' pkgNameSuffix = 'vi.truyenqq' extClass = '.TruyenQQ' - extVersionCode = 9 + extVersionCode = 10 } apply from: "$rootDir/common.gradle" diff --git a/src/vi/truyenqq/src/eu/kanade/tachiyomi/extension/vi/truyenqq/TruyenQQ.kt b/src/vi/truyenqq/src/eu/kanade/tachiyomi/extension/vi/truyenqq/TruyenQQ.kt index 0c7bd7cb6..c41a06db0 100644 --- a/src/vi/truyenqq/src/eu/kanade/tachiyomi/extension/vi/truyenqq/TruyenQQ.kt +++ b/src/vi/truyenqq/src/eu/kanade/tachiyomi/extension/vi/truyenqq/TruyenQQ.kt @@ -1,20 +1,21 @@ package eu.kanade.tachiyomi.extension.vi.truyenqq 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.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 okhttp3.CacheControl import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import org.jsoup.select.Elements import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit @@ -25,7 +26,7 @@ class TruyenQQ : ParsedHttpSource() { override val lang: String = "vi" - override val baseUrl: String = "http://truyenqqhot.com" + override val baseUrl: String = "https://truyenqqhot.com" override val supportsLatest: Boolean = true @@ -36,91 +37,70 @@ class TruyenQQ : ParsedHttpSource() { .followRedirects(true) .build() - override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl) + override fun headersBuilder(): Headers.Builder = + super.headersBuilder().add("Referer", "$baseUrl/") private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US) - private val floatPattern = Regex("""\d+(?:\.\d+)?""") + // Trang html chứa popular + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/truyen-yeu-thich/trang-$page.html", headers) // Selector trả về array các manga (chọn cả ảnh cx được tí nữa parse) override fun popularMangaSelector(): String = "ul.grid > li" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + val anchor = element.selectFirst(".book_info .qtip a") + setUrlWithoutDomain(anchor.attr("href")) + title = anchor.text() + thumbnail_url = element.select(".book_avatar img").attr("abs:src") + } + + // Selector của nút trang kế tiếp + override fun popularMangaNextPageSelector(): String = + ".page_redirect > a:nth-last-child(2) > p:not(.active)" + + // Trang html chứa Latest (các cập nhật mới nhất) + override fun latestUpdatesRequest(page: Int): Request = + GET("$baseUrl/truyen-moi-cap-nhat/trang-$page.html", headers) + // Selector trả về array các manga update (giống selector ở trên) override fun latestUpdatesSelector(): String = popularMangaSelector() - // Selector của nút trang kế tiếp - override fun popularMangaNextPageSelector(): String = ".page_redirect > a:nth-last-child(2) > p:not(.active)" + + override fun latestUpdatesFromElement(element: Element): SManga = + popularMangaFromElement(element) + override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector() - // Trang html chứa popular - override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/truyen-yeu-thich/trang-$page.html", headers) - } - // Trang html chứa Latest (các cập nhật mới nhất) - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/truyen-moi-cap-nhat/trang-$page.html", headers) - } - - // respond là html của trang popular chứ không phải của element đã select - override fun popularMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val imgURL = document.select(".book_avatar img").map { it.attr("abs:src") } - val mangas = document.select(popularMangaSelector()).mapIndexed { index, element -> popularMangaFromElement(element, imgURL[index]) } - - val hasNextPage = popularMangaNextPageSelector().let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) - } - - // Từ 1 element trong list popular đã select ở trên parse thông tin 1 Manga - // Trông code bất ổn nhưng t đang cố làm theo blogtruyen vì t không biết gì hết XD - private fun popularMangaFromElement(element: Element, imgURL: String): SManga { - val manga = SManga.create() - element.select(".book_info .book_name h3 a").first().let { - manga.setUrlWithoutDomain((it.attr("href"))) - manga.title = it.text().trim() - manga.thumbnail_url = imgURL - } - return manga - } - - // Không dùng bản này của fuction nên throw Exception, dùng function ở trên (có 2 params) - override fun popularMangaFromElement(element: Element): SManga = throw Exception("Not Used") - - override fun latestUpdatesFromElement(element: Element): SManga { - val manga = SManga.create() - element.select(".book_info .book_name h3 a").first().let { - manga.setUrlWithoutDomain((it.attr("href"))) - manga.title = it.text().trim() - } - manga.thumbnail_url = element.select(".book_avatar img").first().attr("abs:src") - return manga - } - // Tìm kiếm override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/tim-kiem/trang-$page.html" - val uri = url.toHttpUrlOrNull()!!.newBuilder() - uri.addQueryParameter("q", query) - return GET(uri.toString(), headers) - - // Todo Filters + val url = if (query.isNotBlank()) { + "$baseUrl/tim-kiem/trang-$page.html".toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .build() + .toString() + } else { + val builder = "$baseUrl/tim-kiem-nang-cao/trang-$page.html".toHttpUrl().newBuilder() + (if (filters.isEmpty()) getFilterList() else filters).filterIsInstance() + .forEach { it.addToUri(builder) } + builder.build().toString() + } + return GET(url, headers) } - override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() - override fun searchMangaSelector(): String = popularMangaSelector() - override fun searchMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element) - // Details + override fun searchMangaSelector(): String = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { val info = document.selectFirst(".list-info") + title = document.select("h1").text() author = info.select(".org").joinToString { it.text() } - artist = author - val glist = document.select(".list01 li").map { it.text() } - genre = glist.joinToString() - description = document.select(".story-detail-info").text() + genre = document.select(".list01 li").joinToString { it.text() } + description = document.select(".story-detail-info").textWithLinebreaks() thumbnail_url = document.select("img[itemprop=image]").attr("abs:src") status = when (info.select(".status > p:last-child").text()) { "Đang Cập Nhật" -> SManga.ONGOING @@ -129,29 +109,177 @@ class TruyenQQ : ParsedHttpSource() { } } - // Chapters + private fun Elements.textWithLinebreaks(): String { + this.select("p").prepend("\\n") + this.select("br").prepend("\\n") + return this.text().replace("\\n", "\n").replace("\n ", "\n") + } + // Chapters override fun chapterListSelector(): String = "div.works-chapter-list div.works-chapter-item" + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { - setUrlWithoutDomain(element.select("a").attr("abs:href")) + setUrlWithoutDomain(element.select("a").attr("href")) name = element.select("a").text().trim() date_upload = parseDate(element.select(".time-chap").text()) - chapter_number = floatPattern.find(name)?.value?.toFloatOrNull() ?: -1f - } - private fun parseDate(date: String): Long { - return dateFormat.parse(date)?.time ?: 0L } + private fun parseDate(date: String): Long = kotlin.runCatching { + dateFormat.parse(date)?.time + }.getOrNull() ?: 0L + + override fun pageListRequest(chapter: SChapter): Request = super.pageListRequest(chapter) + .newBuilder() + .cacheControl(CacheControl.FORCE_NETWORK) + .build() + // Pages + override fun pageListParse(document: Document): List = + document.select(".page-chapter img") + .mapIndexed { idx, it -> + Page(idx, imageUrl = it.attr("abs:src")) + } - override fun pageListParse(document: Document): List = mutableListOf().apply { - document.select("img.lazy").forEachIndexed { index, element -> - add(Page(index, "", element.attr("abs:src"))) + override fun imageUrlParse(document: Document): String = + throw UnsupportedOperationException("Not used") + + override fun getFilterList(): FilterList = FilterList( + Filter.Header("Không dùng chung với tìm kiếm bằng tên"), + CountryFilter(), + StatusFilter(), + ChapterCountFilter(), + SortByFilter(), + GenreList(getGenreList()), + ) + + interface UriFilter { + fun addToUri(builder: HttpUrl.Builder) + } + + open class UriPartFilter( + name: String, + private val query: String, + private val vals: Array> + ) : UriFilter, Filter.Select(name, vals.map { it.first }.toTypedArray()) { + override fun addToUri(builder: HttpUrl.Builder) { + builder.addQueryParameter(query, vals[state].second) } } - override fun imageUrlParse(document: Document): String { - throw Exception("Not Used") + + class CountryFilter : UriPartFilter( + "Quốc gia", + "country", + arrayOf( + "Tất cả" to "0", + "Trung Quốc" to "1", + "Việt Nam" to "2", + "Hàn Quốc" to "3", + "Nhật Bản" to "4", + "Mỹ" to "5", + ) + ) + + class StatusFilter : UriPartFilter( + "Tình trạng", + "status", + arrayOf( + "Tất cả" to "-1", + "Đang tiến hành" to "0", + "Hoàn thành" to "2", + ) + ) + + class ChapterCountFilter : UriPartFilter( + "Số lượng chương", + "minchapter", + arrayOf( + "0" to "0", + ">= 100" to "100", + ">= 200" to "200", + ">= 300" to "300", + ">= 400" to "400", + ">= 500" to "500", + ) + ) + + class SortByFilter : UriFilter, Filter.Sort( + "Sắp xếp", + arrayOf("Ngày đăng", "Ngày cập nhật", "Lượt xem"), + Selection(2, false) + ) { + override fun addToUri(builder: HttpUrl.Builder) { + val index = state?.index ?: 2 + val ascending = if (state?.ascending == true) 1 else 0 + builder.addQueryParameter("sort", (index * 2 + ascending).toString()) + } } - // Not Used + class Genre(name: String, val id: String) : Filter.TriState(name) + + class GenreList(state: List) : UriFilter, Filter.Group("Thể loại", state) { + override fun addToUri(builder: HttpUrl.Builder) { + val genres = mutableListOf() + val genresEx = mutableListOf() + + state.forEach { + when (it.state) { + TriState.STATE_INCLUDE -> genres.add(it.id) + TriState.STATE_EXCLUDE -> genresEx.add(it.id) + else -> {} + } + } + + builder.addQueryParameter("category", genres.joinToString(",")) + builder.addQueryParameter("notcategory", genresEx.joinToString(",")) + } + } + + // console.log([...document.querySelectorAll(".genre-item")].map(e => `Genre("${e.innerText}", "${e.querySelector("span").dataset.id}")`).join(",\n")) + private fun getGenreList() = listOf( + Genre("Action", "26"), + Genre("Adventure", "27"), + Genre("Anime", "62"), + Genre("Chuyển Sinh", "91"), + Genre("Cổ Đại", "90"), + Genre("Comedy", "28"), + Genre("Comic", "60"), + Genre("Demons", "99"), + Genre("Detective", "100"), + Genre("Doujinshi", "96"), + Genre("Drama", "29"), + Genre("Fantasy", "30"), + Genre("Gender Bender", "45"), + Genre("Harem", "47"), + Genre("Historical", "51"), + Genre("Horror", "44"), + Genre("Huyền Huyễn", "468"), + Genre("Isekai", "85"), + Genre("Josei", "54"), + Genre("Mafia", "69"), + Genre("Magic", "58"), + Genre("Manhua", "35"), + Genre("Manhwa", "49"), + Genre("Martial Arts", "41"), + Genre("Military", "101"), + Genre("Mystery", "39"), + Genre("Ngôn Tình", "87"), + Genre("One shot", "95"), + Genre("Psychological", "40"), + Genre("Romance", "36"), + Genre("School Life", "37"), + Genre("Sci-fi", "43"), + Genre("Seinen", "42"), + Genre("Shoujo", "38"), + Genre("Shoujo Ai", "98"), + Genre("Shounen", "31"), + Genre("Shounen Ai", "86"), + Genre("Slice of life", "46"), + Genre("Sports", "57"), + Genre("Supernatural", "32"), + Genre("Tragedy", "52"), + Genre("Trọng Sinh", "82"), + Genre("Truyện Màu", "92"), + Genre("Webtoon", "55"), + Genre("Xuyên Không", "88") + ) }