|
|
|
@ -1,10 +1,6 @@
|
|
|
|
package org.koitharu.kotatsu.parsers.site.madara.en
|
|
|
|
package org.koitharu.kotatsu.parsers.site.madara.en
|
|
|
|
|
|
|
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
|
|
|
import kotlinx.coroutines.coroutineScope
|
|
|
|
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
|
|
|
import okhttp3.Headers
|
|
|
|
import okhttp3.Headers
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.RequestBody
|
|
|
|
import okhttp3.RequestBody
|
|
|
|
@ -16,170 +12,170 @@ import org.koitharu.kotatsu.parsers.model.*
|
|
|
|
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
|
|
|
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
|
|
|
import org.koitharu.kotatsu.parsers.util.*
|
|
|
|
import org.koitharu.kotatsu.parsers.util.*
|
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
import java.util.EnumSet
|
|
|
|
import java.util.*
|
|
|
|
|
|
|
|
|
|
|
|
@MangaSourceParser("ADULT_WEBTOON", "AdultWebtoon", "en", ContentType.HENTAI)
|
|
|
|
@MangaSourceParser("ADULT_WEBTOON", "AdultWebtoon", "en", ContentType.HENTAI)
|
|
|
|
internal class AdultWebtoon(context: MangaLoaderContext) :
|
|
|
|
internal class AdultWebtoon(context: MangaLoaderContext) :
|
|
|
|
MadaraParser(context, MangaParserSource.ADULT_WEBTOON, "adultwebtoon.com") {
|
|
|
|
MadaraParser(context, MangaParserSource.ADULT_WEBTOON, "adultwebtoon.com") {
|
|
|
|
override val tagPrefix = "adult-webtoon-genre/"
|
|
|
|
override val tagPrefix = "adult-webtoon-genre/"
|
|
|
|
override val listUrl = "adult-webtoon/"
|
|
|
|
override val listUrl = "adult-webtoon/"
|
|
|
|
override val postReq = true
|
|
|
|
override val postReq = true
|
|
|
|
override val withoutAjax = true
|
|
|
|
override val withoutAjax = true
|
|
|
|
override val availableSortOrders: Set<SortOrder> =
|
|
|
|
override val availableSortOrders: Set<SortOrder> =
|
|
|
|
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
|
|
|
|
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.RATING)
|
|
|
|
|
|
|
|
|
|
|
|
override val filterCapabilities: MangaListFilterCapabilities
|
|
|
|
override val filterCapabilities: MangaListFilterCapabilities
|
|
|
|
get() = super.filterCapabilities.copy(
|
|
|
|
get() = super.filterCapabilities.copy(
|
|
|
|
isTagsExclusionSupported = false,
|
|
|
|
isTagsExclusionSupported = false,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
|
|
|
|
override suspend fun getFilterOptions() = super.getFilterOptions().copy(
|
|
|
|
availableStates = emptySet(),
|
|
|
|
availableStates = emptySet(),
|
|
|
|
availableContentRating = emptySet(),
|
|
|
|
availableContentRating = emptySet(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilterV2): List<Manga> {
|
|
|
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
|
|
|
val pages = page + 1
|
|
|
|
val pages = page + 1
|
|
|
|
|
|
|
|
|
|
|
|
val url = buildString {
|
|
|
|
val url = buildString {
|
|
|
|
append("https://")
|
|
|
|
append("https://")
|
|
|
|
append(domain)
|
|
|
|
append(domain)
|
|
|
|
when {
|
|
|
|
when {
|
|
|
|
!filter.query.isNullOrEmpty() -> {
|
|
|
|
!filter.query.isNullOrEmpty() -> {
|
|
|
|
if (pages > 1) {
|
|
|
|
if (pages > 1) {
|
|
|
|
append("/page/")
|
|
|
|
append("/page/")
|
|
|
|
append(pages.toString())
|
|
|
|
append(pages.toString())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
append("/?s=")
|
|
|
|
append("/?s=")
|
|
|
|
append(filter.query.urlEncoded())
|
|
|
|
append(filter.query.urlEncoded())
|
|
|
|
append("&post_type=wp-manga")
|
|
|
|
append("&post_type=wp-manga")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
|
|
if (filter.tags.isNotEmpty()) {
|
|
|
|
if (filter.tags.isNotEmpty()) {
|
|
|
|
filter.tags.oneOrThrowIfMany()?.let {
|
|
|
|
filter.tags.oneOrThrowIfMany()?.let {
|
|
|
|
append('/')
|
|
|
|
append('/')
|
|
|
|
append(tagPrefix)
|
|
|
|
append(tagPrefix)
|
|
|
|
append(it.key)
|
|
|
|
append(it.key)
|
|
|
|
append('/')
|
|
|
|
append('/')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
append('/')
|
|
|
|
append('/')
|
|
|
|
append(listUrl)
|
|
|
|
append(listUrl)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pages > 1) {
|
|
|
|
if (pages > 1) {
|
|
|
|
append("page/")
|
|
|
|
append("page/")
|
|
|
|
append(pages)
|
|
|
|
append(pages)
|
|
|
|
append('/')
|
|
|
|
append('/')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
append("?m_orderby=")
|
|
|
|
append("?m_orderby=")
|
|
|
|
when (order) {
|
|
|
|
when (order) {
|
|
|
|
SortOrder.POPULARITY -> append("views")
|
|
|
|
SortOrder.POPULARITY -> append("views")
|
|
|
|
SortOrder.UPDATED -> append("latest")
|
|
|
|
SortOrder.UPDATED -> append("latest")
|
|
|
|
SortOrder.NEWEST -> append("new-manga")
|
|
|
|
SortOrder.NEWEST -> append("new-manga")
|
|
|
|
SortOrder.ALPHABETICAL -> append("alphabet")
|
|
|
|
SortOrder.ALPHABETICAL -> append("alphabet")
|
|
|
|
SortOrder.RATING -> append("rating")
|
|
|
|
SortOrder.RATING -> append("rating")
|
|
|
|
else -> append("latest")
|
|
|
|
else -> append("latest")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return parseMangaList(webClient.httpGet(url).parseHtml())
|
|
|
|
return parseMangaList(webClient.httpGet(url).parseHtml())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
|
|
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
|
|
|
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
|
|
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
|
|
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
|
|
|
val body = doc.body()
|
|
|
|
val body = doc.body()
|
|
|
|
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
|
|
|
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
|
|
|
val desc = body.select(selectDesc).html()
|
|
|
|
val desc = body.select(selectDesc).html()
|
|
|
|
val stateDiv = if (selectState.isEmpty()) {
|
|
|
|
val stateDiv = if (selectState.isEmpty()) {
|
|
|
|
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
|
|
|
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
body.selectFirst(selectState)
|
|
|
|
body.selectFirst(selectState)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val state = stateDiv?.let {
|
|
|
|
val state = stateDiv?.let {
|
|
|
|
when (it.text()) {
|
|
|
|
when (it.text()) {
|
|
|
|
in ongoing -> MangaState.ONGOING
|
|
|
|
in ongoing -> MangaState.ONGOING
|
|
|
|
in finished -> MangaState.FINISHED
|
|
|
|
in finished -> MangaState.FINISHED
|
|
|
|
in abandoned -> MangaState.ABANDONED
|
|
|
|
in abandoned -> MangaState.ABANDONED
|
|
|
|
in paused -> MangaState.PAUSED
|
|
|
|
in paused -> MangaState.PAUSED
|
|
|
|
else -> null
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val alt =
|
|
|
|
val alt =
|
|
|
|
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
|
|
|
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
|
|
|
?.trim()
|
|
|
|
?.trim()
|
|
|
|
|
|
|
|
|
|
|
|
manga.copy(
|
|
|
|
manga.copy(
|
|
|
|
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
|
|
|
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
|
|
|
MangaTag(
|
|
|
|
MangaTag(
|
|
|
|
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
|
|
|
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
|
|
|
title = a.text().toTitleCase(),
|
|
|
|
title = a.text().toTitleCase(),
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
description = desc,
|
|
|
|
description = desc,
|
|
|
|
altTitle = alt,
|
|
|
|
altTitle = alt,
|
|
|
|
state = state,
|
|
|
|
state = state,
|
|
|
|
chapters = chaptersDeferred.await(),
|
|
|
|
chapters = chaptersDeferred.await(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
|
|
|
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
|
|
|
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
|
|
|
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
|
|
|
val url = "https://$domain/wp-admin/admin-ajax.php"
|
|
|
|
val url = "https://$domain/wp-admin/admin-ajax.php"
|
|
|
|
val postData = "post_id=$mangaId&action=ajax_chap"
|
|
|
|
val postData = "post_id=$mangaId&action=ajax_chap"
|
|
|
|
val headers = Headers.Builder().add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()
|
|
|
|
val headers = Headers.Builder().add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()
|
|
|
|
val doc = makeRequest(url, postData.toRequestBody(), headers)
|
|
|
|
val doc = makeRequest(url, postData.toRequestBody(), headers)
|
|
|
|
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
|
|
|
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
|
|
|
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
|
|
|
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
|
|
|
val a = li.selectFirst("a")
|
|
|
|
val a = li.selectFirst("a")
|
|
|
|
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
|
|
|
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
|
|
|
val link = href + stylePage
|
|
|
|
val link = href + stylePage
|
|
|
|
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
|
|
|
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
|
|
|
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
|
|
|
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
|
|
|
MangaChapter(
|
|
|
|
MangaChapter(
|
|
|
|
id = generateUid(href),
|
|
|
|
id = generateUid(href),
|
|
|
|
url = link,
|
|
|
|
url = link,
|
|
|
|
name = name,
|
|
|
|
name = name,
|
|
|
|
number = i + 1f,
|
|
|
|
number = i + 1f,
|
|
|
|
volume = 0,
|
|
|
|
volume = 0,
|
|
|
|
branch = null,
|
|
|
|
branch = null,
|
|
|
|
uploadDate = parseChapterDate(
|
|
|
|
uploadDate = parseChapterDate(
|
|
|
|
dateFormat,
|
|
|
|
dateFormat,
|
|
|
|
dateText,
|
|
|
|
dateText,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
scanlator = null,
|
|
|
|
scanlator = null,
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
|
|
|
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
|
|
|
var retryCount = 0
|
|
|
|
var retryCount = 0
|
|
|
|
val backoffDelay = 2000L // Initial delay (milliseconds)
|
|
|
|
val backoffDelay = 2000L // Initial delay (milliseconds)
|
|
|
|
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
|
|
|
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
|
|
|
while (true) {
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
return context.httpClient.newCall(request).execute().parseHtml()
|
|
|
|
return context.httpClient.newCall(request).execute().parseHtml()
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
} catch (e: Exception) {
|
|
|
|
// Log or handle the exception as needed
|
|
|
|
// Log or handle the exception as needed
|
|
|
|
if (++retryCount <= 5) {
|
|
|
|
if (++retryCount <= 5) {
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
delay(backoffDelay)
|
|
|
|
delay(backoffDelay)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
throw e
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|