Add source GocTruyenTranhVui (#9728)
* Add GocTruyenTranhVui * Use jsonInstance * Use parseAs * Use HttpSource() * Merge DTO files * Using chapterListParse and loginRequired * Fix variable * Use toManga(), Use toChapter(), fix no chapter * Fix Url, Works well * Add Advanced search * Optimize variable naming & add mangaId cache * MangaIdCache: Add limit cache * Apply suggestion * Fix package declaration, format using Android Studio * Fix names: use camel case Dto instead of DTO even if it's acronym; add S in to[S]Manga/Chapter * Use generic ResultDto<T> to replace similar classes * Inline the typealiases, which are used to demonstrate how to use generics * More conventional namings * Change manga url format; override getMangaUrl; fix chapterListParse slug which is definitely not tested; remove useless HTML parse fallback * Use timestamp value from API instead of parsing string * Early abort in pageListParse() * Refactor filters; don't get an empty filter list if argument is empty, it's uselss * Parse more manga fields from API and set initialized because all fields are filled; fix listing next page; use selectFirst()!!.text() instead of select().text() * Use search endpoint for latest updates; the home endpoint doesn't provide genres --------- Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
parent
88749bec9f
commit
ffff87d5a0
8
src/vi/goctruyentranhvui/build.gradle
Normal file
8
src/vi/goctruyentranhvui/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Goc Truyen Tranh Vui'
|
||||
extClass = '.GocTruyenTranhVui'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/vi/goctruyentranhvui/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/vi/goctruyentranhvui/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
src/vi/goctruyentranhvui/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/vi/goctruyentranhvui/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
src/vi/goctruyentranhvui/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/vi/goctruyentranhvui/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
BIN
src/vi/goctruyentranhvui/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/vi/goctruyentranhvui/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/vi/goctruyentranhvui/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/vi/goctruyentranhvui/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,70 @@
|
||||
package eu.kanade.tachiyomi.extension.vi.goctruyentranhvui
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ResultDto<T>(
|
||||
val result: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
private val numberChapter: String,
|
||||
private val updateTime: Long,
|
||||
) {
|
||||
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
|
||||
name = numberChapter
|
||||
date_upload = updateTime
|
||||
url = "/truyen/$slug/chuong-$numberChapter"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterListDto(
|
||||
val chapters: List<ChapterDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ListingDto(
|
||||
val next: Boolean,
|
||||
val data: List<MangaDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
private val id: String,
|
||||
private val name: String,
|
||||
private val description: String,
|
||||
private val statusCode: String,
|
||||
private val photo: String,
|
||||
private val nameEn: String,
|
||||
private val author: String,
|
||||
private val category: List<String>? = null,
|
||||
) {
|
||||
fun toSManga(baseUrl: String): SManga = SManga.create().apply {
|
||||
title = name
|
||||
thumbnail_url = baseUrl + photo
|
||||
url = "$id:$nameEn"
|
||||
author = this@MangaDto.author
|
||||
description = this@MangaDto.description
|
||||
genre = category?.joinToString()
|
||||
status = when (statusCode) {
|
||||
"PRG" -> SManga.ONGOING
|
||||
"END" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ImageListWrapper(
|
||||
val body: ResultDto<ImageListDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ImageListDto(
|
||||
val data: List<String>,
|
||||
)
|
@ -0,0 +1,74 @@
|
||||
package eu.kanade.tachiyomi.extension.vi.goctruyentranhvui
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class Option(name: String, val id: String) : Filter.CheckBox(name)
|
||||
open class FilterGroup(name: String, val query: String, state: List<Option>) : Filter.Group<Option>(name, state)
|
||||
|
||||
class StatusList(status: List<Option>) : FilterGroup("Trạng Thái", "status[]", status)
|
||||
|
||||
fun getStatusList() = listOf(
|
||||
Option("Đang thực hiện", "PRG"),
|
||||
Option("Hoàn thành", "END"),
|
||||
Option("Truyện Chữ", "novel"),
|
||||
)
|
||||
|
||||
class SortByList(sort: List<Option>) : FilterGroup("Sắp xếp", "orders[]", sort)
|
||||
|
||||
fun getSortByList() = listOf(
|
||||
Option("Lượt xem", "viewCount"),
|
||||
Option("Lượt đánh giá", "evaluationScore"),
|
||||
Option("Lượt theo dõi", "followerCount"),
|
||||
Option("Ngày Cập Nhật", "recentDate"),
|
||||
Option("Truyện Mới", "createdAt"),
|
||||
)
|
||||
|
||||
class GenreList(genres: List<Option>) : FilterGroup("Thể loại", "categories[]", genres)
|
||||
|
||||
fun getGenreList() = listOf(
|
||||
Option("Anime", "ANI"),
|
||||
Option("Drama", "DRA"),
|
||||
Option("Josei", "JOS"),
|
||||
Option("Manhwa", "MAW"),
|
||||
Option("One Shot", "OSH"),
|
||||
Option("Shounen", "SHO"),
|
||||
Option("Webtoons", "WEB"),
|
||||
Option("Shoujo", "SHJ"),
|
||||
Option("Harem", "HAR"),
|
||||
Option("Ecchi", "ECC"),
|
||||
Option("Mature", "MAT"),
|
||||
Option("Slice of life", "SOL"),
|
||||
Option("Isekai", "ISE"),
|
||||
Option("Manga", "MAG"),
|
||||
Option("Manhua", "MAU"),
|
||||
Option("Hành Động", "ACT"),
|
||||
Option("Phiêu Lưu", "ADV"),
|
||||
Option("Hài Hước", "COM"),
|
||||
Option("Võ Thuật", "MAA"),
|
||||
Option("Huyền Bí", "MYS"),
|
||||
Option("Lãng Mạn", "ROM"),
|
||||
Option("Thể Thao", "SPO"),
|
||||
Option("Học Đường", "SCL"),
|
||||
Option("Lịch Sử", "HIS"),
|
||||
Option("Kinh Dị", "HOR"),
|
||||
Option("Siêu Nhiên", "SUN"),
|
||||
Option("Bi Kịch", "TRA"),
|
||||
Option("Trùng Sinh", "RED"),
|
||||
Option("Game", "GAM"),
|
||||
Option("Viễn Tưởng", "FTS"),
|
||||
Option("Khoa Học", "SCF"),
|
||||
Option("Truyện Màu", "COI"),
|
||||
Option("Người Lớn", "ADU"),
|
||||
Option("BoyLove", "BBL"),
|
||||
Option("Hầm Ngục", "DUN"),
|
||||
Option("Săn Bắn", "HUNT"),
|
||||
Option("Ngôn Từ Nhạy Cảm", "NTNC"),
|
||||
Option("Doujinshi", "DOU"),
|
||||
Option("Bạo Lực", "BLM"),
|
||||
Option("Ngôn Tình", "NTT"),
|
||||
Option("Nữ Cường", "NCT"),
|
||||
Option("Gender Bender", "GDB"),
|
||||
Option("Murim", "MRR"),
|
||||
Option("Leo Tháp", "LTT"),
|
||||
Option("Nấu Ăn", "COO"),
|
||||
)
|
@ -0,0 +1,138 @@
|
||||
package eu.kanade.tachiyomi.extension.vi.goctruyentranhvui
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class GocTruyenTranhVui : HttpSource() {
|
||||
override val lang = "vi"
|
||||
|
||||
override val baseUrl = "https://goctruyentranhvui17.com"
|
||||
|
||||
override val name = "Goc Truyen Tranh Vui"
|
||||
|
||||
private val apiUrl = "$baseUrl/api/v2"
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(
|
||||
apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("home/filter")
|
||||
addQueryParameter("p", (page - 1).toString())
|
||||
addQueryParameter("value", "recommend")
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val res = response.parseAs<ResultDto<ListingDto>>()
|
||||
val hasNextPage = res.result.next
|
||||
return MangasPage(res.result.data.map { it.toSManga(baseUrl) }, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(
|
||||
"$apiUrl/search?p=${page - 1}&orders%5B%5D=recentDate",
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/truyen/${manga.url.substringAfter(':')}"
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.substringBefore(':')
|
||||
val slug = manga.url.substringAfter(':')
|
||||
return GET("$baseUrl/api/comic/$mangaId/chapter?limit=-1#$slug", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val slug = response.request.url.fragment!!
|
||||
val chapterJson = response.parseAs<ResultDto<ChapterListDto>>()
|
||||
return chapterJson.result.chapters.map { it.toSChapter(slug) }
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(getMangaUrl(manga), headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
title = document.selectFirst(".v-card-title")!!.text()
|
||||
genre = document.select(".group-content > .v-chip-link").joinToString { it.text() }
|
||||
thumbnail_url = document.selectFirst("img.image")!!.absUrl("src")
|
||||
status = parseStatus(document.selectFirst(".mb-1:contains(Trạng thái:) span")!!.text())
|
||||
author = document.selectFirst(".mb-1:contains(Tác giả:) span")!!.text()
|
||||
description = document.selectFirst(".v-card-text")!!.text()
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("Đang thực hiện", ignoreCase = true) -> SManga.ONGOING
|
||||
status.contains("Hoàn thành", ignoreCase = true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body.string()
|
||||
val pattern = Regex("chapterJson:\\s*`(.*?)`")
|
||||
val match = pattern.find(html) ?: throw Exception("Không tìm thấy Json") // find json
|
||||
val jsonPage = match.groups[1]!!.value
|
||||
if (jsonPage.isEmpty()) throw Exception("Không có nội dung. Hãy đăng nhập trong WebView") // loginRequired
|
||||
val result = jsonPage.parseAs<ImageListWrapper>()
|
||||
val imageList = result.body.result.data
|
||||
return imageList.mapIndexed { i, url ->
|
||||
val finalUrl = if (url.startsWith("/image/")) {
|
||||
baseUrl + url
|
||||
} else {
|
||||
url
|
||||
}
|
||||
Page(i, imageUrl = finalUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("search")
|
||||
addQueryParameter("p", (page - 1).toString())
|
||||
addQueryParameter("searchValue", query)
|
||||
for (filter in filters) {
|
||||
when (filter) {
|
||||
is FilterGroup ->
|
||||
for (checkbox in filter.state) {
|
||||
if (checkbox.state) addQueryParameter(filter.query, checkbox.id)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
StatusList(getStatusList()),
|
||||
SortByList(getSortByList()),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user