parent
c949b95dab
commit
2fff92b023
@ -1 +1 @@
|
||||
total: 1204
|
||||
total: 1205
|
||||
@ -0,0 +1,197 @@
|
||||
package org.koitharu.kotatsu.parsers.site.vi
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.core.LegacyPagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HANGTRUYEN", "Hang Truyện", "vi")
|
||||
internal class HangTruyen(context: MangaLoaderContext) : LegacyPagedMangaParser(context, MangaParserSource.HANGTRUYEN, 10) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("hangtruyen.org")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.UPDATED_ASC,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.NEWEST_ASC,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.POPULARITY_ASC,
|
||||
)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
||||
availableTags = fetchAvailableTags(),
|
||||
availableContentTypes = EnumSet.of(
|
||||
ContentType.MANGA,
|
||||
ContentType.MANHWA,
|
||||
ContentType.MANHUA,
|
||||
ContentType.COMICS,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
val url = buildString {
|
||||
append("/tim-kiem?page=")
|
||||
append(page)
|
||||
|
||||
if (filter.types.isNotEmpty()) {
|
||||
append("&categoryIds=")
|
||||
val categoryIds = filter.types.joinToString(",") { type ->
|
||||
when (type) {
|
||||
ContentType.MANGA -> "1"
|
||||
ContentType.MANHUA -> "2"
|
||||
ContentType.MANHWA -> "3"
|
||||
ContentType.COMICS -> "4,5"
|
||||
else -> "1,2,3,4,5"
|
||||
}
|
||||
}
|
||||
append(categoryIds)
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&genreIds=")
|
||||
filter.tags.joinTo(this, separator = ",") { it.key }
|
||||
}
|
||||
|
||||
append("&orderBy=")
|
||||
append(when (order) {
|
||||
SortOrder.POPULARITY -> "view_desc"
|
||||
SortOrder.POPULARITY_ASC -> "view_asc"
|
||||
SortOrder.UPDATED -> "udpated_at_date_desc"
|
||||
SortOrder.UPDATED_ASC -> "udpated_at_date_asc"
|
||||
SortOrder.NEWEST -> "created_at_date_desc"
|
||||
SortOrder.NEWEST_ASC -> "created_at_date_asc"
|
||||
else -> "view_desc"
|
||||
})
|
||||
|
||||
if (!filter.query.isNullOrEmpty()) {
|
||||
append("&keyword=")
|
||||
val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part ->
|
||||
part.urlEncoded()
|
||||
}
|
||||
append(encodedQuery)
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.select("div.m-post.col-md-6").map { div ->
|
||||
val href = div.selectFirst("h3.m-name a")?.attrAsRelativeUrl("href") ?: ""
|
||||
val ratingText = div.selectFirst("span")?.text()?.toFloatOrNull() ?: 0f
|
||||
val rating = (ratingText / 5f) * 5f
|
||||
val img = div.selectFirst("img.lzl")?.let { img ->
|
||||
img.attr("data-src").takeUnless { it.isNullOrEmpty() } ?: img.attr("data-original")
|
||||
}
|
||||
val title = div.selectFirst("h3.m-name a")?.text().orEmpty()
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = title,
|
||||
altTitles = emptySet(),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = rating,
|
||||
contentRating = if (isNsfwSource) ContentRating.ADULT else ContentRating.SAFE,
|
||||
coverUrl = img,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
authors = emptySet(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
|
||||
val script = doc.selectFirst("script:containsData(const mangaDetail)")?.data() ?: return manga
|
||||
val mangaDetailJson = script.substringAfter("const mangaDetail = ").substringBefore(";")
|
||||
val mangaDetail = JSONObject(mangaDetailJson)
|
||||
|
||||
val mangaSlug = mangaDetail.getString("slug")
|
||||
val adultTagIds = setOf("29", "31", "210", "211", "175", "41", "212")
|
||||
|
||||
val tags = mangaDetail.getJSONArray("genres").mapJSONToSet { genre ->
|
||||
MangaTag(
|
||||
key = genre.getInt("id").toString(),
|
||||
title = genre.getString("name").toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
val isAdult = tags.any { it.key in adultTagIds }
|
||||
|
||||
return manga.copy(
|
||||
title = mangaDetail.getString("title").orEmpty(),
|
||||
authors = mangaDetail.optString("author").takeUnless { it.isNullOrEmpty() }?.let { setOf(it) } ?: emptySet(),
|
||||
tags = tags,
|
||||
description = mangaDetail.getString("overview").orEmpty(),
|
||||
state = when (mangaDetail.optInt("status")) {
|
||||
0 -> MangaState.ONGOING
|
||||
1 -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
contentRating = if (isAdult) ContentRating.ADULT else ContentRating.SAFE,
|
||||
chapters = mangaDetail.getJSONArray("chapters").mapJSON { chapter ->
|
||||
val chapterSlug = chapter.getString("slug")
|
||||
val chapterUrl = "$mangaSlug/$chapterSlug"
|
||||
MangaChapter(
|
||||
id = generateUid(chapterUrl),
|
||||
title = chapter.getString("name"),
|
||||
number = chapter.getDouble("index").toFloat(),
|
||||
url = chapterUrl,
|
||||
scanlator = null,
|
||||
uploadDate = dateFormat.tryParse(chapter.getString("releasedAt")),
|
||||
branch = null,
|
||||
source = source,
|
||||
volume = 0
|
||||
)
|
||||
}.reversed()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val script = doc.selectFirst("script:containsData(let mangaDetail)")?.data() ?: return emptyList()
|
||||
val chapterDetailJson = script.substringAfter("chapterDetail=").substringBefore("}</script>") + "}"
|
||||
val chapterDetail = JSONObject(chapterDetailJson)
|
||||
|
||||
return chapterDetail.getJSONArray("images").mapJSON { image ->
|
||||
val url = image.getString("path")
|
||||
val index = image.getInt("index")
|
||||
index to url
|
||||
}.sortedBy { it.first }.map { (_, url) ->
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/tim-kiem").parseHtml()
|
||||
return doc.select("div.list-genres span").mapToSet {
|
||||
MangaTag(
|
||||
key = it.attr("data-value"),
|
||||
title = it.text().replace("#", "").toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue