fix getlist VmpParser, LineWebtoonsParser, BlogTruyenParser, HentaiVNParser, YurinekoParser
add filter on WpComicsParser, ZMangaParser, LxManga, Truyenqq, TruyentranhLHParser Move NetTruyen to template WpComicsParser and add multitags on WpComicsParser
This commit is contained in:
@@ -132,83 +132,117 @@ internal abstract class LineWebtoonsParser(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val genre = tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val manga =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
|
||||
.getJSONObject("challengeSearch")
|
||||
.getJSONArray("titleList")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
val sortOrderStr = when (sortOrder) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = null,
|
||||
tags = emptySet(),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val manga = if (query != null) {
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("This source does not support search with tags")
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
|
||||
val sortOrderStr = when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
|
||||
}
|
||||
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
|
||||
.getJSONObject("challengeSearch")
|
||||
.getJSONArray("titleList")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = null,
|
||||
tags = emptySet(),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
@@ -309,10 +343,10 @@ internal abstract class LineWebtoonsParser(
|
||||
}
|
||||
|
||||
fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) {
|
||||
val msgpad = Calendar.getInstance().timeInMillis.toString()
|
||||
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad))
|
||||
val msgPad = Calendar.getInstance().timeInMillis.toString()
|
||||
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgPad))
|
||||
urlBuilder
|
||||
.addQueryParameter("msgpad", msgpad)
|
||||
.addQueryParameter("msgpad", msgPad)
|
||||
.addQueryParameter("md", digest)
|
||||
// .addEncodedQueryParameter("md", digest.urlEncoded())
|
||||
}
|
||||
|
||||
@@ -180,15 +180,16 @@ internal abstract class MadaraParser(
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("/$tagPrefix")
|
||||
append(tag?.key.orEmpty())
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/$tagPrefix")
|
||||
append(it.key)
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?")
|
||||
}
|
||||
append("/?")
|
||||
} else {
|
||||
|
||||
if (pages > 1) {
|
||||
@@ -241,8 +242,9 @@ internal abstract class MadaraParser(
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
payload["vars[wp-manga-genre]"] = it.key
|
||||
}
|
||||
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
|
||||
|
||||
@@ -29,9 +29,118 @@ class BlogTruyenParser(context: MangaLoaderContext) :
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US)
|
||||
private var cacheTags = SuspendLazy(::fetchTags)
|
||||
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
return when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${filter.query.urlEncoded()}&p=$page"
|
||||
val searchContent = webClient.httpGet(searchUrl).parseHtml()
|
||||
.selectFirst("section.list-manga-bycate > div.list")
|
||||
parseMangaList(searchContent)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany().let {
|
||||
val categoryAjax =
|
||||
"https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${it?.key}&orderBy=5&p=$page"
|
||||
val listContent = webClient.httpGet(categoryAjax).parseHtml().selectFirst("div.list")
|
||||
parseMangaList(listContent)
|
||||
}
|
||||
} else {
|
||||
getNormalList(page)
|
||||
}
|
||||
}
|
||||
|
||||
null -> getNormalList(page)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNormalList(page: Int): List<Manga> {
|
||||
val pageLink = "https://${domain}/page-$page"
|
||||
val doc = webClient.httpGet(pageLink).parseHtml()
|
||||
val listElements = doc.selectFirstOrThrow("section.list-mainpage.listview")
|
||||
.select("div.bg-white.storyitem")
|
||||
|
||||
return listElements.mapNotNull { el ->
|
||||
val linkTag = el.selectFirst("div.fl-l > a") ?: return@mapNotNull null
|
||||
val relativeUrl = linkTag.attrAsRelativeUrl("href")
|
||||
val tags = cacheTags.tryGet().getOrNull()?.let { tagMap ->
|
||||
el.select("footer > div.category > a").mapNotNullToSet { a ->
|
||||
tagMap[a.text()]
|
||||
}
|
||||
}
|
||||
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = linkTag.attr("title"),
|
||||
altTitle = null,
|
||||
description = el.selectFirst("p.al-j.break.line-height-15")?.text(),
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
coverUrl = linkTag.selectLast("img")?.src().orEmpty(),
|
||||
source = source,
|
||||
tags = tags ?: emptySet(),
|
||||
isNsfw = false,
|
||||
rating = RATING_UNKNOWN,
|
||||
author = null,
|
||||
state = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaList(listElement: Element?): List<Manga> {
|
||||
listElement ?: return emptyList()
|
||||
|
||||
return listElement.select("span.tiptip[data-tiptip]").mapNotNull {
|
||||
val mangaInfo = listElement.getElementById(it.attr("data-tiptip")) ?: return@mapNotNull null
|
||||
val a = it.selectFirst("a") ?: return@mapNotNull null
|
||||
val relativeUrl = a.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = a.text(),
|
||||
altTitle = null,
|
||||
description = mangaInfo.select("div.al-j.fs-12").text(),
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
coverUrl = mangaInfo.selectFirst("div > img.img")?.src().orEmpty(),
|
||||
isNsfw = false,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return cacheTags.get().values.toSet()
|
||||
}
|
||||
|
||||
private suspend fun fetchTags(): Map<String, MangaTag> {
|
||||
val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagItems = doc.select("li[data-id]")
|
||||
val tagMap = ArrayMap<String, MangaTag>(tagItems.size)
|
||||
for (tag in tagItems) {
|
||||
val title = tag.text().trim()
|
||||
tagMap[tag.text().trim()] = MangaTag(
|
||||
title = title,
|
||||
key = tag.attr("data-id"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
return tagMap
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val descriptionElement = doc.selectFirstOrThrow("div.description")
|
||||
@@ -70,7 +179,7 @@ class BlogTruyenParser(context: MangaLoaderContext) :
|
||||
author = descriptionElement.selectFirst("p:contains(Tác giả) > a")?.text(),
|
||||
description = doc.selectFirst(".detail .content")?.html(),
|
||||
chapters = parseChapterList(doc),
|
||||
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.imageUrl(),
|
||||
largeCoverUrl = doc.selectLast("div.thumbnail > img")?.src().orEmpty(),
|
||||
state = state,
|
||||
rating = rating ?: RATING_UNKNOWN,
|
||||
isNsfw = doc.getElementById("warningCategory") != null,
|
||||
@@ -98,90 +207,6 @@ class BlogTruyenParser(context: MangaLoaderContext) :
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
return when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
val searchUrl = "https://${domain}/timkiem/nangcao/1/0/-1/-1?txt=${query.urlEncoded()}&p=$page"
|
||||
val searchContent = webClient.httpGet(searchUrl).parseHtml()
|
||||
.selectFirst("section.list-manga-bycate > div.list")
|
||||
parseMangaList(searchContent)
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
val tag = tags.oneOrThrowIfMany()!!
|
||||
val categoryAjax =
|
||||
"https://${domain}/ajax/Category/AjaxLoadMangaByCategory?id=${tag.key}&orderBy=5&p=$page"
|
||||
val listContent = webClient.httpGet(categoryAjax).parseHtml().selectFirst("div.list")
|
||||
parseMangaList(listContent)
|
||||
}
|
||||
|
||||
else -> getNormalList(page)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNormalList(page: Int): List<Manga> {
|
||||
val pageLink = "https://${domain}/page-$page"
|
||||
val doc = webClient.httpGet(pageLink).parseHtml()
|
||||
val listElements = doc.selectFirstOrThrow("section.list-mainpage.listview")
|
||||
.select("div.bg-white.storyitem")
|
||||
|
||||
return listElements.mapNotNull { el ->
|
||||
val linkTag = el.selectFirst("div.fl-l > a") ?: return@mapNotNull null
|
||||
val relativeUrl = linkTag.attrAsRelativeUrl("href")
|
||||
val tags = cacheTags.tryGet().getOrNull()?.let { tagMap ->
|
||||
el.select("footer > div.category > a").mapNotNullToSet { a ->
|
||||
tagMap[a.text()]
|
||||
}
|
||||
}
|
||||
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = linkTag.attr("title"),
|
||||
altTitle = null,
|
||||
description = el.selectFirst("p.al-j.break.line-height-15")?.text(),
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
coverUrl = linkTag.selectLast("img")?.imageUrl().orEmpty(),
|
||||
source = source,
|
||||
tags = tags ?: emptySet(),
|
||||
isNsfw = false,
|
||||
rating = RATING_UNKNOWN,
|
||||
author = null,
|
||||
state = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaList(listElement: Element?): List<Manga> {
|
||||
listElement ?: return emptyList()
|
||||
|
||||
return listElement.select("span.tiptip[data-tiptip]").mapNotNull {
|
||||
val mangaInfo = listElement.getElementById(it.attr("data-tiptip")) ?: return@mapNotNull null
|
||||
val a = it.selectFirst("a") ?: return@mapNotNull null
|
||||
val relativeUrl = a.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = a.text(),
|
||||
altTitle = null,
|
||||
description = mangaInfo.select("div.al-j.fs-12").text(),
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
coverUrl = mangaInfo.selectFirst("div > img.img")?.imageUrl().orEmpty(),
|
||||
isNsfw = false,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
fun generateImageId(index: Int) = generateUid("${chapter.url}/$index")
|
||||
|
||||
@@ -191,7 +216,7 @@ class BlogTruyenParser(context: MangaLoaderContext) :
|
||||
pages.add(
|
||||
MangaPage(
|
||||
id = generateImageId(pages.size),
|
||||
url = img.imageUrl(),
|
||||
url = img.src().orEmpty(),
|
||||
preview = null,
|
||||
source = source,
|
||||
),
|
||||
@@ -218,30 +243,4 @@ class BlogTruyenParser(context: MangaLoaderContext) :
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return cacheTags.get().values.toSet()
|
||||
}
|
||||
|
||||
|
||||
private suspend fun fetchTags(): Map<String, MangaTag> {
|
||||
val doc = webClient.httpGet("/timkiem/nangcao".toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagItems = doc.select("li[data-id]")
|
||||
val tagMap = ArrayMap<String, MangaTag>(tagItems.size)
|
||||
for (tag in tagItems) {
|
||||
val title = tag.text().trim()
|
||||
tagMap[tag.text().trim()] = MangaTag(
|
||||
title = title,
|
||||
key = tag.attr("data-id"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
return tagMap
|
||||
}
|
||||
|
||||
private fun Element.imageUrl(): String {
|
||||
return attrAsAbsoluteUrlOrNull("src")
|
||||
?: attrAsAbsoluteUrlOrNull("data-cfsrc")
|
||||
?: ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,57 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
return when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1
|
||||
urlBuilder()
|
||||
val searchUrl =
|
||||
"/tim-kiem-truyen.html?key=${filter.query.urlEncoded()}&page=$page".toAbsoluteUrl(domain)
|
||||
val docs = webClient.httpGet(searchUrl).parseHtml()
|
||||
parseMainList(docs, page)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val pageSize = if (filter.tags.isEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE
|
||||
val page = (offset / pageSize.toFloat()).toIntUp() + 1
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val url = buildString {
|
||||
val tagKey = "tag[]".urlEncoded()
|
||||
append("/forum/search-plus.php?name=")
|
||||
append("&dou=&char=")
|
||||
filter.tags.forEach { tag ->
|
||||
append("&")
|
||||
append(tagKey)
|
||||
append("=")
|
||||
append(tag.key)
|
||||
}
|
||||
append("&search=")
|
||||
append("&page=")
|
||||
append(page)
|
||||
}.toAbsoluteUrl(domain)
|
||||
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
return parseAdvanceSearch(docs, page)
|
||||
} else {
|
||||
val site = if (filter.sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach"
|
||||
val url = "$site.html?page=$page".toAbsoluteUrl(domain)
|
||||
context.cookieJar.insertCookies(domain, *getSortCookies(filter.sortOrder))
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
parseMainList(docs, page)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
val page = (offset / PAGE_SIZE.toFloat()).toIntUp() + 1
|
||||
val url = "/chap-moi.html?page=$page".toAbsoluteUrl(domain)
|
||||
parseMainList(webClient.httpGet(url).parseHtml(), page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val chapterDeferred = async { fetchChapters(manga.url) }
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
@@ -79,48 +130,6 @@ class HentaiVNParser(context: MangaLoaderContext) : MangaParser(context, MangaSo
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
|
||||
val pageSize = if (tags.isNullOrEmpty()) PAGE_SIZE else SEARCH_PAGE_SIZE
|
||||
val page = (offset / pageSize.toFloat()).toIntUp() + 1
|
||||
return when {
|
||||
!tags.isNullOrEmpty() -> {
|
||||
val url = buildString {
|
||||
val tagKey = "tag[]".urlEncoded()
|
||||
append("/forum/search-plus.php?name=")
|
||||
append(query?.urlEncoded().orEmpty())
|
||||
append("&dou=&char=")
|
||||
tags.forEach { tag ->
|
||||
append("&")
|
||||
append(tagKey)
|
||||
append("=")
|
||||
append(tag.key)
|
||||
}
|
||||
append("&search=")
|
||||
append("&page=")
|
||||
append(page)
|
||||
}.toAbsoluteUrl(domain)
|
||||
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
return parseAdvanceSearch(docs, page)
|
||||
}
|
||||
|
||||
!query.isNullOrEmpty() -> {
|
||||
urlBuilder()
|
||||
val searchUrl = "/tim-kiem-truyen.html?key=${query.urlEncoded()}&page=$page".toAbsoluteUrl(domain)
|
||||
val docs = webClient.httpGet(searchUrl).parseHtml()
|
||||
parseMainList(docs, page)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val site = if (sortOrder == SortOrder.UPDATED) "/chap-moi" else "/danh-sach"
|
||||
val url = "$site.html?page=$page".toAbsoluteUrl(domain)
|
||||
context.cookieJar.insertCookies(domain, *getSortCookies(sortOrder))
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
parseMainList(docs, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val docs = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return docs.select("#image > img").map {
|
||||
|
||||
@@ -20,57 +20,76 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("lxmanga.net")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
val skey = "filter[name]=".urlEncoded()
|
||||
append("/tim-kiem?$skey")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/the-loai/")
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/the-loai/")
|
||||
append(it.key)
|
||||
}
|
||||
} else {
|
||||
append("/danh-sach")
|
||||
}
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
|
||||
if (filter.states.isNotEmpty()) {
|
||||
append("&filter[status]=")
|
||||
filter.states.forEach {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "2,"
|
||||
MangaState.FINISHED -> "1,"
|
||||
else -> "1,2"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
append("&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("-views")
|
||||
SortOrder.UPDATED -> append("-updated_at")
|
||||
SortOrder.NEWEST -> append("-created_at")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("-updated_at")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
append("/danh-sach")
|
||||
null -> {
|
||||
append("/danh-sach?sort=-updated_at&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("-views")
|
||||
SortOrder.UPDATED -> append("-updated_at")
|
||||
SortOrder.NEWEST -> append("-created_at")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
|
||||
else -> append("-updated_at")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
|
||||
return doc.select("div.grid div.manga-vertical")
|
||||
.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attr("href")
|
||||
@@ -95,7 +114,6 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||
|
||||
return manga.copy(
|
||||
altTitle = root.select(".divider2:contains(Noms associés :)").firstOrNull()?.text(),
|
||||
state = when (root.select("div.grow div.mt-2:contains(Tình trạng) a").first()!!.text()) {
|
||||
@@ -135,9 +153,7 @@ internal class LxManga(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
return doc.select("div.text-center img.lazy").map { img ->
|
||||
val url = img.attrAsRelativeUrlOrNull("data-src") ?: img.attrAsRelativeUrlOrNull("src")
|
||||
?: img.parseFailed("Image src not found")
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.vi
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi")
|
||||
class NetTruyenParser(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.NETTRUYEN, pageSize = 36) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
|
||||
"www.nettruyenlive.com",
|
||||
"www.nettruyenio.com",
|
||||
"www.nettruyento.com",
|
||||
"nettruyento.com",
|
||||
"nettruyenin.com",
|
||||
)
|
||||
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.RATING)
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val dateFormat = SimpleDateFormat("dd/MM/yy", Locale.US)
|
||||
private var tagCache: ArrayMap<String, MangaTag>? = null
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val rating = doc.selectFirst("span[itemprop=ratingValue]")
|
||||
?.ownText()
|
||||
?.toFloatOrNull() ?: 0f
|
||||
|
||||
val chapterElements = doc.getElementById("nt_listchapter")?.select("ul > li") ?: doc.parseFailed()
|
||||
val chapters = chapterElements.mapChapters(reversed = true) { index, element ->
|
||||
val a = element.selectFirst("div.chapter > a") ?: return@mapChapters null
|
||||
val relativeUrl = a.attrAsRelativeUrlOrNull("href") ?: return@mapChapters null
|
||||
val timeText = element.selectFirst("div.col-xs-4.text-center.no-wrap.small")?.text()
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(relativeUrl),
|
||||
name = a.text(),
|
||||
number = index + 1,
|
||||
url = relativeUrl,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterTime(timeText),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
return manga.copy(
|
||||
rating = rating / 5,
|
||||
chapters = chapters,
|
||||
description = doc.selectFirst("div.detail-content > p")?.html(),
|
||||
isNsfw = doc.selectFirst("div.alert.alert-danger > strong:contains(Cảnh báo độ tuổi)") != null,
|
||||
)
|
||||
}
|
||||
|
||||
// 20 giây trước
|
||||
// 52 phút trước
|
||||
// 6 giờ trước
|
||||
// 2 ngày trước
|
||||
// 19:09 30/07
|
||||
// 23/12/21
|
||||
private fun parseChapterTime(timeText: String?): Long {
|
||||
if (timeText.isNullOrEmpty()) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
val timeWords = arrayOf("giây", "phút", "giờ", "ngày")
|
||||
val calendar = Calendar.getInstance()
|
||||
val timeArr = timeText.split(' ')
|
||||
if (WordSet(*timeWords).anyWordIn(timeText)) {
|
||||
val timeSuffix = timeArr.getOrNull(1)
|
||||
val timeDiff = timeArr.getOrNull(0)?.toIntOrNull() ?: return 0L
|
||||
when (timeSuffix) {
|
||||
timeWords[0] -> calendar.add(Calendar.SECOND, -timeDiff)
|
||||
timeWords[1] -> calendar.add(Calendar.MINUTE, -timeDiff)
|
||||
timeWords[2] -> calendar.add(Calendar.HOUR, -timeDiff)
|
||||
timeWords[3] -> calendar.add(Calendar.DATE, -timeDiff)
|
||||
else -> return 0L
|
||||
}
|
||||
} else {
|
||||
val relativeDate = timeArr.lastOrNull() ?: return 0L
|
||||
val dateString = when (relativeDate.split('/').size) {
|
||||
2 -> {
|
||||
val currentYear = calendar.get(Calendar.YEAR).toString().takeLast(2)
|
||||
"$relativeDate/$currentYear"
|
||||
}
|
||||
|
||||
3 -> relativeDate
|
||||
else -> return 0L
|
||||
}
|
||||
|
||||
calendar.timeInMillis = dateFormat.tryParse(dateString)
|
||||
}
|
||||
|
||||
|
||||
return calendar.time.time
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val isSearching = !query.isNullOrEmpty()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (isSearching) {
|
||||
append("/tim-truyen?keyword=")
|
||||
append(query!!.urlEncoded())
|
||||
append("&page=")
|
||||
append(page)
|
||||
} else {
|
||||
val tagQuery = tags.orEmpty().joinToString(",") { it.key }
|
||||
append("/tim-truyen-nang-cao?genres=$tagQuery")
|
||||
append("¬genres=&gender=-1&status=-1&minchapter=1&sort=${getSortOrderKey(sortOrder)}")
|
||||
append("&page=$page")
|
||||
}
|
||||
}
|
||||
|
||||
val response = if (isSearching) {
|
||||
val result = runCatchingCancellable { webClient.httpGet(url) }
|
||||
val exception = result.exceptionOrNull()
|
||||
if (exception is NotFoundException) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
result.getOrThrow()
|
||||
} else {
|
||||
webClient.httpGet(url)
|
||||
}
|
||||
|
||||
val itemsElements = response.parseHtml()
|
||||
.select("div.ModuleContent > div.items")
|
||||
.select("div.item")
|
||||
return itemsElements.mapNotNull { item ->
|
||||
val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null
|
||||
val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null
|
||||
val slug = absUrl.substringAfterLast('/')
|
||||
val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) {
|
||||
"Đang tiến hành" -> MangaState.ONGOING
|
||||
"Hoàn thành" -> MangaState.FINISHED
|
||||
else -> null
|
||||
}
|
||||
|
||||
val tagMap = getOrCreateTagMap()
|
||||
val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty()
|
||||
val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] }
|
||||
Manga(
|
||||
id = generateUid(slug),
|
||||
title = tooltipElement.selectFirst("div.title")?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
url = absUrl.toRelativeUrl(domain),
|
||||
publicUrl = absUrl,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(),
|
||||
largeCoverUrl = null,
|
||||
tags = mangaTags,
|
||||
state = mangaState,
|
||||
author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(),
|
||||
description = tooltipElement.selectFirst("div.box_text")?.text(),
|
||||
chapters = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val pageElements = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
.select("div.reading-detail.box_doc > div img")
|
||||
return pageElements.map { element ->
|
||||
val url = element.attrAsAbsoluteUrl("data-original")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val map = getOrCreateTagMap()
|
||||
val tagSet = ArraySet<MangaTag>(map.size)
|
||||
for (entry in map) {
|
||||
tagSet.add(entry.value)
|
||||
}
|
||||
|
||||
return tagSet
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
|
||||
tagCache?.let { return@withLock it }
|
||||
val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagItems = doc.select("div.genre-item")
|
||||
val result = ArrayMap<String, MangaTag>(tagItems.size)
|
||||
for (item in tagItems) {
|
||||
val title = item.text().trim()
|
||||
val key = item.select("span[data-id]").attr("data-id")
|
||||
result[title] = MangaTag(title = title, key = key, source = source)
|
||||
}
|
||||
tagCache = result
|
||||
result
|
||||
}
|
||||
|
||||
private fun getSortOrderKey(sortOrder: SortOrder) = when (sortOrder) {
|
||||
SortOrder.UPDATED -> 0
|
||||
SortOrder.POPULARITY -> 10
|
||||
SortOrder.NEWEST -> 15
|
||||
SortOrder.RATING -> 20
|
||||
else -> throw IllegalArgumentException("Sort order ${sortOrder.name} not supported")
|
||||
}
|
||||
}
|
||||
@@ -14,40 +14,70 @@ internal class Truyenqq(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("truyenqqvn.com")
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tagQuery = if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = ",") { it.key }
|
||||
val url = if (!query.isNullOrEmpty()) {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem/trang-$page.html")
|
||||
append("?q=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem-nang-cao/trang-$page.html")
|
||||
append("?status=-1&country=0&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("4")
|
||||
SortOrder.UPDATED -> append("2")
|
||||
SortOrder.NEWEST -> append("0")
|
||||
else -> append("2")
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem/trang-$page.html")
|
||||
append("?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem-nang-cao/trang-")
|
||||
append(page.toString())
|
||||
append(".html?country=0&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("4")
|
||||
SortOrder.UPDATED -> append("2")
|
||||
SortOrder.NEWEST -> append("0")
|
||||
else -> append("2")
|
||||
}
|
||||
if (filter.states.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "0"
|
||||
MangaState.FINISHED -> "1"
|
||||
else -> "-1"
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
append("&status=-1")
|
||||
}
|
||||
|
||||
append("&category=")
|
||||
append(filter.tags.joinToString(separator = ",") { it.key })
|
||||
append("¬category=&minchapter=0")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem-nang-cao/trang-")
|
||||
append(page.toString())
|
||||
append(".html?status=-1&country=0&sort=2&category=¬category=&minchapter=0")
|
||||
}
|
||||
|
||||
}
|
||||
append("&category=")
|
||||
append(tagQuery)
|
||||
append("¬category=&minchapter=0")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.requireElementById("main_homepage").select("li").map { li ->
|
||||
val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
|
||||
@@ -19,10 +19,84 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("truyentranhlh.net")
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var tagCache: Map<String, MangaTag>? = null
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = urlBuilder().apply {
|
||||
addPathSegment("tim-kiem")
|
||||
addQueryParameter("page", page.toString())
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
addQueryParameter("q", filter.query)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
addQueryParameter(
|
||||
"sort",
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.NEWEST -> "new"
|
||||
SortOrder.RATING -> "like"
|
||||
SortOrder.POPULARITY -> "top"
|
||||
SortOrder.ALPHABETICAL -> "az"
|
||||
},
|
||||
)
|
||||
|
||||
if (filter.states.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
addQueryParameter(
|
||||
"status",
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "3"
|
||||
MangaState.PAUSED -> "2"
|
||||
else -> "0"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tagsQuery = filter.tags.joinToString(separator = ",") { it.key }
|
||||
addEncodedQueryParameter("accept_genres", tagsQuery)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
addQueryParameter("sort", "update")
|
||||
}
|
||||
}
|
||||
|
||||
}.build()
|
||||
|
||||
return webClient.httpGet(url).parseHtml()
|
||||
.select(".container .card.card-dark .row > .thumb-item-flow")
|
||||
.mapNotNull {
|
||||
val a = it.selectFirstOrThrow(".thumb-wrapper a")
|
||||
Manga(
|
||||
id = generateUid(a.attrAsRelativeUrl("href")),
|
||||
url = a.attrAsRelativeUrl("href"),
|
||||
publicUrl = a.attrAsAbsoluteUrl("href"),
|
||||
title = it.select(".thumb_attr.series-title").text(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val infoHeaderEl = docs.selectFirst("main.section-body")
|
||||
@@ -33,6 +107,7 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
|
||||
val state = when (infoEl?.selectFirst(".info-item:contains(Tình trạng) > .info-value")?.text()) {
|
||||
"Đang tiến hành" -> MangaState.ONGOING
|
||||
"Đã hoàn thành" -> MangaState.FINISHED
|
||||
"Tạm ngưng" -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
val rating = infoHeaderEl?.let {
|
||||
@@ -68,53 +143,6 @@ class TruyentranhLHParser(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val sortQuery = when (sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.NEWEST -> "new"
|
||||
SortOrder.RATING -> "like"
|
||||
SortOrder.POPULARITY -> "top"
|
||||
SortOrder.ALPHABETICAL -> "az"
|
||||
}
|
||||
val url = urlBuilder().apply {
|
||||
addPathSegment("tim-kiem")
|
||||
addQueryParameter("sort", sortQuery)
|
||||
addQueryParameter("page", page.toString())
|
||||
if (!query.isNullOrEmpty()) {
|
||||
addQueryParameter("q", query)
|
||||
}
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
val tagsQuery = tags.joinToString(separator = ",") { it.key }
|
||||
addEncodedQueryParameter("accept_genres", tagsQuery)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return webClient.httpGet(url).parseHtml()
|
||||
.select(".container .card.card-dark .row > .thumb-item-flow")
|
||||
.mapNotNull {
|
||||
val a = it.selectFirstOrThrow(".thumb-wrapper a")
|
||||
Manga(
|
||||
id = generateUid(a.attrAsRelativeUrl("href")),
|
||||
url = a.attrAsRelativeUrl("href"),
|
||||
publicUrl = a.attrAsAbsoluteUrl("href"),
|
||||
title = it.select(".thumb_attr.series-title").text(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = a.selectFirst("div[data-bg]")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val url = chapter.url.toAbsoluteUrl(domain)
|
||||
return webClient.httpGet(url).parseHtml().select("#chapter-content > img").mapNotNull {
|
||||
|
||||
@@ -5,30 +5,12 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.json.asIterable
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||
import org.koitharu.kotatsu.parsers.util.json.*
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||
import org.koitharu.kotatsu.parsers.util.requireElementById
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import org.koitharu.kotatsu.parsers.util.tryParse
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
@@ -44,45 +26,25 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma
|
||||
private val apiDomain
|
||||
get() = "api.$domain"
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson()
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
return manga.copy(
|
||||
chapters = response.getJSONArray("chapters")
|
||||
.toJSONList()
|
||||
.mapChapters(true) { i, jo ->
|
||||
val mangaId = jo.getInt("mangaID")
|
||||
val chapterId = jo.getInt("id")
|
||||
MangaChapter(
|
||||
id = generateUid(chapterId.toLong()),
|
||||
name = jo.getString("name"),
|
||||
number = i + 1,
|
||||
scanlator = null,
|
||||
url = "/read/$mangaId/$chapterId",
|
||||
uploadDate = df.tryParse(jo.getString("date")),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val listUrl =
|
||||
when (filter) {
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val listUrl = when {
|
||||
!query.isNullOrEmpty() -> "/search?query=${query.urlEncoded()}&page=$page"
|
||||
tags.isNullOrEmpty() -> "/lastest2?page=$page"
|
||||
tags.size == 1 -> "/searchType?type=tag&id=${tags.first().key}&page=$page"
|
||||
else -> {
|
||||
// Sort order is different when filter with multiple tags
|
||||
val tagKeys = tags.joinToString(separator = ",") { it.key }
|
||||
"/advancedSearch?genre=$tagKeys¬Genre=&sort=7&minChapter=1&status=0&page=$page"
|
||||
is MangaListFilter.Search -> {
|
||||
"/search?query=${filter.query.urlEncoded()}&page=$page"
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tagKeys = filter.tags.joinToString(separator = ",") { it.key }
|
||||
"/advancedSearch?genre=$tagKeys¬Genre=&sort=7&minChapter=1&status=0&page=$page"
|
||||
} else {
|
||||
"/lastest2?page=$page"
|
||||
}
|
||||
}
|
||||
|
||||
null -> "/lastest2?page=$page"
|
||||
}
|
||||
}
|
||||
val jsonResponse = webClient.httpGet(listUrl.toAbsoluteUrl(apiDomain)).parseJson()
|
||||
return jsonResponse.getJSONArray("result")
|
||||
.mapJSON { jo ->
|
||||
@@ -119,6 +81,29 @@ class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, Ma
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson()
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
return manga.copy(
|
||||
chapters = response.getJSONArray("chapters")
|
||||
.toJSONList()
|
||||
.mapChapters(true) { i, jo ->
|
||||
val mangaId = jo.getInt("mangaID")
|
||||
val chapterId = jo.getInt("id")
|
||||
MangaChapter(
|
||||
id = generateUid(chapterId.toLong()),
|
||||
name = jo.getString("name"),
|
||||
number = i + 1,
|
||||
scanlator = null,
|
||||
url = "/read/$mangaId/$chapterId",
|
||||
uploadDate = df.tryParse(jo.getString("date")),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val jsonData = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
.requireElementById("__NEXT_DATA__")
|
||||
|
||||
@@ -16,8 +16,11 @@ internal abstract class VmpParser(
|
||||
) : PagedMangaParser(context, source, pageSize) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
protected open val listUrl = "xxx/"
|
||||
protected open val geneUrl = "genero/"
|
||||
|
||||
@@ -26,27 +29,42 @@ internal abstract class VmpParser(
|
||||
searchPaginator.firstPage = 1
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://$domain/")
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append(geneUrl)
|
||||
append(tag?.key.orEmpty())
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
} else {
|
||||
append(listUrl)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append('/')
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append(listUrl)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("?s=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(geneUrl)
|
||||
append(it.key)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
} else {
|
||||
append(listUrl)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append(listUrl)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package org.koitharu.kotatsu.parsers.site.wpcomics
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.DateFormat
|
||||
@@ -21,15 +26,16 @@ internal abstract class WpComicsParser(
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
protected open val listUrl = "/the-loai"
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
protected open val listUrl = "/tim-truyen-nang-cao"
|
||||
protected open val datePattern = "dd/MM/yy"
|
||||
|
||||
|
||||
@@ -48,75 +54,136 @@ internal abstract class WpComicsParser(
|
||||
@JvmField
|
||||
protected val finished: Set<String> = setOf(
|
||||
"Hoàn thành",
|
||||
"Completed ",
|
||||
"Completed",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append(listUrl)
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val response =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-truyen?keyword=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/")
|
||||
append(tag?.key.orEmpty())
|
||||
val result = runCatchingCancellable { webClient.httpGet(url) }
|
||||
val exception = result.exceptionOrNull()
|
||||
if (exception is NotFoundException) {
|
||||
return emptyList()
|
||||
}
|
||||
result.getOrThrow()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
val tagQuery = filter.tags.joinToString(",") { it.key }
|
||||
append("/tim-truyen-nang-cao?genres=")
|
||||
append(tagQuery)
|
||||
append("¬genres=&gender=-1&minchapter=1&sort=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> 0
|
||||
SortOrder.POPULARITY -> 10
|
||||
SortOrder.NEWEST -> 15
|
||||
SortOrder.RATING -> 20
|
||||
else -> throw IllegalArgumentException("Sort order ${filter.sortOrder.name} not supported")
|
||||
},
|
||||
)
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "2"
|
||||
else -> "-1"
|
||||
},
|
||||
)
|
||||
}
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
webClient.httpGet(url)
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-truyen-nang-cao?genres=¬genres=&gender=-1&status=-1&minchapter=1&sort=0&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
webClient.httpGet(url)
|
||||
}
|
||||
}
|
||||
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&keyword=")
|
||||
append(query.urlEncoded())
|
||||
val itemsElements = response.parseHtml()
|
||||
.select("div.ModuleContent > div.items")
|
||||
.select("div.item")
|
||||
return itemsElements.mapNotNull { item ->
|
||||
val tooltipElement = item.selectFirst("div.box_tootip") ?: return@mapNotNull null
|
||||
val absUrl = item.selectFirst("div.image > a")?.attrAsAbsoluteUrlOrNull("href") ?: return@mapNotNull null
|
||||
val slug = absUrl.substringAfterLast('/')
|
||||
val mangaState = when (tooltipElement.selectFirst("div.message_main > p:contains(Tình trạng)")?.ownText()) {
|
||||
"Đang tiến hành" -> MangaState.ONGOING
|
||||
"Hoàn thành" -> MangaState.FINISHED
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("10")
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.NEWEST -> append("15")
|
||||
else -> append("")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return doc.select("div.item").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
val tagMap = getOrCreateTagMap()
|
||||
val tagsElement = tooltipElement.selectFirst("div.message_main > p:contains(Thể loại)")?.ownText().orEmpty()
|
||||
val mangaTags = tagsElement.split(',').mapNotNullToSet { tagMap[it.trim()] }
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = div.selectFirst("img")?.src().orEmpty(),
|
||||
title = div.selectFirstOrThrow("h3").text().orEmpty(),
|
||||
id = generateUid(slug),
|
||||
title = tooltipElement.selectFirst("div.title")?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
url = absUrl.toRelativeUrl(domain),
|
||||
publicUrl = absUrl,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
isNsfw = false,
|
||||
coverUrl = item.selectFirst("div.image a img")?.absUrl("data-original").orEmpty(),
|
||||
largeCoverUrl = null,
|
||||
tags = mangaTags,
|
||||
state = mangaState,
|
||||
author = tooltipElement.selectFirst("div.message_main > p:contains(Tác giả)")?.ownText(),
|
||||
description = tooltipElement.selectFirst("div.box_text")?.text(),
|
||||
chapters = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
|
||||
return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li ->
|
||||
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
|
||||
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
val map = getOrCreateTagMap()
|
||||
val tagSet = ArraySet<MangaTag>(map.size)
|
||||
for (entry in map) {
|
||||
tagSet.add(entry.value)
|
||||
}
|
||||
return tagSet
|
||||
}
|
||||
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var tagCache: ArrayMap<String, MangaTag>? = null
|
||||
|
||||
private suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
|
||||
tagCache?.let { return@withLock it }
|
||||
val doc = webClient.httpGet("/tim-truyen-nang-cao".toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagItems = doc.select("div.genre-item")
|
||||
val result = ArrayMap<String, MangaTag>(tagItems.size)
|
||||
for (item in tagItems) {
|
||||
val title = item.text().trim()
|
||||
val key = item.select("span[data-id]").attr("data-id")
|
||||
result[title] = MangaTag(title = title, key = key, source = source)
|
||||
}
|
||||
tagCache = result
|
||||
result
|
||||
}
|
||||
|
||||
protected open val selectDesc = "div.detail-content p"
|
||||
@@ -127,13 +194,9 @@ internal abstract class WpComicsParser(
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val chaptersDeferred = async { getChapters(doc) }
|
||||
|
||||
val desc = doc.selectFirstOrThrow(selectDesc).html()
|
||||
|
||||
val stateDiv = doc.selectFirst(selectState)
|
||||
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
@@ -141,17 +204,8 @@ internal abstract class WpComicsParser(
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val aut = doc.body().select(selectAut).text()
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = null,
|
||||
author = aut,
|
||||
@@ -165,19 +219,16 @@ internal abstract class WpComicsParser(
|
||||
protected open val selectChapter = "div#nt_listchapter li:not(.heading)"
|
||||
|
||||
protected open suspend fun getChapters(doc: Document): List<MangaChapter> {
|
||||
|
||||
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
val dateText = li.selectFirst(selectDate)?.text()
|
||||
|
||||
val findHours = dateText?.contains(":")
|
||||
val dateFormat = if (findHours == true) {
|
||||
SimpleDateFormat("HH:mm dd/MM", sourceLocale)
|
||||
} else {
|
||||
SimpleDateFormat(datePattern, sourceLocale)
|
||||
}
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
name = a.text(),
|
||||
@@ -295,5 +346,4 @@ internal abstract class WpComicsParser(
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.site.wpcomics.en
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
@@ -9,11 +11,13 @@ import java.util.EnumSet
|
||||
|
||||
@MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS)
|
||||
internal class XoxoComics(context: MangaLoaderContext) :
|
||||
WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomics.net", 50) {
|
||||
WpComicsParser(context, MangaSource.XOXOCOMICS, "xoxocomic.com", 50) {
|
||||
|
||||
override val listUrl = "/genre"
|
||||
override val listUrl = "/comic-list"
|
||||
override val datePattern = "MM/dd/yyyy"
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.NEWEST,
|
||||
@@ -21,46 +25,66 @@ internal class XoxoComics(context: MangaLoaderContext) :
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("/search?keyword=")
|
||||
append(query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
} else {
|
||||
append(listUrl)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/")
|
||||
append(tag?.key.orEmpty())
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search-comic?keyword=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
append("/")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular")
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.NEWEST -> append("newest")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
else -> append("")
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/")
|
||||
append(it.key)
|
||||
}
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "/ongoing"
|
||||
MangaState.FINISHED -> "/completed"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
if (filter.tags.isEmpty()) {
|
||||
append("-comic")
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.states.isEmpty() && filter.tags.isEmpty()) {
|
||||
append(listUrl)
|
||||
}
|
||||
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("/popular")
|
||||
SortOrder.UPDATED -> append("/latest")
|
||||
SortOrder.NEWEST -> append("/newest")
|
||||
SortOrder.ALPHABETICAL -> append("")
|
||||
else -> append("/latest")
|
||||
}
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
|
||||
null -> {
|
||||
append(listUrl)
|
||||
append("/?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return doc.select("div.item").map { div ->
|
||||
return doc.select("div.item, #nt_listchapter nav ul li").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
@@ -79,12 +103,53 @@ internal class XoxoComics(context: MangaLoaderContext) :
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
|
||||
return doc.select("div.genres ul li:not(.active)").mapNotNullToSet { li ->
|
||||
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
|
||||
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val chaptersDeferred = async { getChapters(doc) }
|
||||
val desc = doc.selectFirstOrThrow(selectDesc).html()
|
||||
val stateDiv = doc.selectFirst(selectState)
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val aut = doc.body().select(selectAut).text()
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = null,
|
||||
author = aut,
|
||||
state = state,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all"
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
|
||||
return doc.select(selectPage).map { url ->
|
||||
val img = url.src()?.toRelativeUrl(domain) ?: url.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.parsers.site.wpcomics.vi
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
|
||||
|
||||
@MangaSourceParser("NETTRUYEN", "NetTruyen", "vi")
|
||||
internal class NetTruyen(context: MangaLoaderContext) :
|
||||
WpComicsParser(context, MangaSource.NETTRUYEN, "www.nettruyenlive.com", 36) {
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
|
||||
"www.nettruyenlive.com",
|
||||
"www.nettruyenio.com",
|
||||
"www.nettruyento.com",
|
||||
"nettruyento.com",
|
||||
"nettruyenin.com",
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
|
||||
|
||||
@MangaSourceParser("NETTRUYENMAX", "NettruyenMax", "vi")
|
||||
internal class Nettruyenmax(context: MangaLoaderContext) :
|
||||
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36) {
|
||||
override val listUrl = "/tim-truyen"
|
||||
}
|
||||
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenus.com", 36)
|
||||
|
||||
@@ -21,13 +21,9 @@ internal abstract class ZMangaParser(
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
protected open val listUrl = "advanced-search/"
|
||||
protected open val datePattern = "MMMM d, yyyy"
|
||||
@@ -50,42 +46,56 @@ internal abstract class ZMangaParser(
|
||||
"Completed",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/$listUrl")
|
||||
append('/')
|
||||
append(listUrl)
|
||||
if (page > 1) {
|
||||
append("page/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
append('/')
|
||||
}
|
||||
|
||||
append("?order=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular")
|
||||
SortOrder.UPDATED -> append("update")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
SortOrder.NEWEST -> append("latest")
|
||||
SortOrder.RATING -> append("rating")
|
||||
}
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&title=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
when (filter) {
|
||||
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
for (tag in tags) {
|
||||
append("&")
|
||||
append("genre[]".urlEncoded())
|
||||
append("=")
|
||||
append(tag.key)
|
||||
is MangaListFilter.Search -> {
|
||||
append("&title=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("?order=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular")
|
||||
SortOrder.UPDATED -> append("update")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
SortOrder.NEWEST -> append("latest")
|
||||
SortOrder.RATING -> append("rating")
|
||||
}
|
||||
|
||||
filter.tags.forEach {
|
||||
append("&")
|
||||
append("genre[]".urlEncoded())
|
||||
append("=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("?order=update")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user