[TruyenQQ] Refactor, add filters, fix empty page list (#15065)

This commit is contained in:
beerpsi 2023-01-22 19:30:40 +07:00 committed by GitHub
parent 1754456d2e
commit 34285655aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 212 additions and 84 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'TruyenQQ'
pkgNameSuffix = 'vi.truyenqq'
extClass = '.TruyenQQ'
extVersionCode = 9
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View File

@ -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<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 {
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<Page> = mutableListOf<Page>().apply {
document.select("img.lazy").forEachIndexed { index, element ->
add(Page(index, "", element.attr("abs:src")))
}
}
override fun imageUrlParse(document: Document): String {
throw Exception("Not Used")
override fun pageListParse(document: Document): List<Page> =
document.select(".page-chapter img")
.mapIndexed { idx, it ->
Page(idx, imageUrl = it.attr("abs:src"))
}
// Not Used
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<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)
}
}
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())
}
}
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")
)
}