[TruyenQQ] Refactor, add filters, fix empty page list (#15065)
This commit is contained in:
parent
1754456d2e
commit
34285655aa
|
@ -5,7 +5,7 @@ ext {
|
||||||
extName = 'TruyenQQ'
|
extName = 'TruyenQQ'
|
||||||
pkgNameSuffix = 'vi.truyenqq'
|
pkgNameSuffix = 'vi.truyenqq'
|
||||||
extClass = '.TruyenQQ'
|
extClass = '.TruyenQQ'
|
||||||
extVersionCode = 9
|
extVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
package eu.kanade.tachiyomi.extension.vi.truyenqq
|
package eu.kanade.tachiyomi.extension.vi.truyenqq
|
||||||
|
|
||||||
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.FilterList
|
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.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 okhttp3.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Elements
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -25,7 +26,7 @@ class TruyenQQ : ParsedHttpSource() {
|
||||||
|
|
||||||
override val lang: String = "vi"
|
override val lang: String = "vi"
|
||||||
|
|
||||||
override val baseUrl: String = "http://truyenqqhot.com"
|
override val baseUrl: String = "https://truyenqqhot.com"
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
@ -36,91 +37,70 @@ class TruyenQQ : ParsedHttpSource() {
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
.build()
|
.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 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)
|
// 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 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)
|
// Selector trả về array các manga update (giống selector ở trên)
|
||||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
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()
|
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
|
// Tìm kiếm
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/tim-kiem/trang-$page.html"
|
val url = if (query.isNotBlank()) {
|
||||||
val uri = url.toHttpUrlOrNull()!!.newBuilder()
|
"$baseUrl/tim-kiem/trang-$page.html".toHttpUrl().newBuilder()
|
||||||
uri.addQueryParameter("q", query)
|
.addQueryParameter("q", query)
|
||||||
return GET(uri.toString(), headers)
|
.build()
|
||||||
|
.toString()
|
||||||
// Todo Filters
|
} else {
|
||||||
|
val builder = "$baseUrl/tim-kiem-nang-cao/trang-$page.html".toHttpUrl().newBuilder()
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<UriFilter>()
|
||||||
|
.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 {
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
val info = document.selectFirst(".list-info")
|
val info = document.selectFirst(".list-info")
|
||||||
|
|
||||||
title = document.select("h1").text()
|
title = document.select("h1").text()
|
||||||
author = info.select(".org").joinToString { it.text() }
|
author = info.select(".org").joinToString { it.text() }
|
||||||
artist = author
|
genre = document.select(".list01 li").joinToString { it.text() }
|
||||||
val glist = document.select(".list01 li").map { it.text() }
|
description = document.select(".story-detail-info").textWithLinebreaks()
|
||||||
genre = glist.joinToString()
|
|
||||||
description = document.select(".story-detail-info").text()
|
|
||||||
thumbnail_url = document.select("img[itemprop=image]").attr("abs:src")
|
thumbnail_url = document.select("img[itemprop=image]").attr("abs:src")
|
||||||
status = when (info.select(".status > p:last-child").text()) {
|
status = when (info.select(".status > p:last-child").text()) {
|
||||||
"Đang Cập Nhật" -> SManga.ONGOING
|
"Đ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 chapterListSelector(): String = "div.works-chapter-list div.works-chapter-item"
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
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()
|
name = element.select("a").text().trim()
|
||||||
date_upload = parseDate(element.select(".time-chap").text())
|
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
|
// Pages
|
||||||
|
override fun pageListParse(document: Document): List<Page> =
|
||||||
|
document.select(".page-chapter img")
|
||||||
|
.mapIndexed { idx, it ->
|
||||||
|
Page(idx, imageUrl = it.attr("abs:src"))
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
override fun imageUrlParse(document: Document): String =
|
||||||
document.select("img.lazy").forEachIndexed { index, element ->
|
throw UnsupportedOperationException("Not used")
|
||||||
add(Page(index, "", element.attr("abs:src")))
|
|
||||||
|
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<Pair<String, String>>
|
||||||
|
) : UriFilter, Filter.Select<String>(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<Genre>) : UriFilter, Filter.Group<Genre>("Thể loại", state) {
|
||||||
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
|
val genres = mutableListOf<String>()
|
||||||
|
val genresEx = mutableListOf<String>()
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue