Fix Sources
Add sources Correct detect login on MadaraParser close #988 fix oocini.biz close #987 Changing the latest DEPRECATION ( Technically we can raise the old support for getList() and MangaChapter() )master
parent
71affd155c
commit
1f7fe2aed3
@ -0,0 +1,172 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
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.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("MANGAKAWAII_EN", "MangaKawaii En", "en")
|
||||
internal class MangaKawaiiEn(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaParserSource.MANGAKAWAII_EN, 50) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("www.mangakawaii.io")
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("Accept-Language", "en")
|
||||
.build()
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?query=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&search_type=manga&page=")
|
||||
append(page)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.sortOrder == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
|
||||
throw IllegalArgumentException("Filter part tag is not available with sort not updated")
|
||||
}
|
||||
|
||||
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/manga-list")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/category/")
|
||||
append(it.key)
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("li.section__list-group-item").ifEmpty {
|
||||
doc.select("div.media-thumbnail")
|
||||
}.map { div ->
|
||||
val a = div.selectFirstOrThrow("a")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = (div.selectFirst("img")?.src() ?: a.attr("data-bg")).orEmpty(),
|
||||
title = div.selectFirstOrThrow("h4, .media-thumbnail__name").text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val firstChapter = doc.selectFirst("tr[class*='volume-'] a")?.attr("href")
|
||||
val chaptersDeferred = async { loadChapters(firstChapter) }
|
||||
manga.copy(
|
||||
description = doc.selectFirst("dd.text-justify.text-break")?.text().orEmpty(),
|
||||
altTitle = doc.select("span[itemprop*=alternativeHeadline]").joinToString { ", " },
|
||||
author = doc.select("a[href*=author]").text(),
|
||||
state = when (doc.selectFirst("span.badge.bg-success.text-uppercase")?.text()) {
|
||||
"Ongoing" -> MangaState.ONGOING
|
||||
"" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
tags = doc.select("a[href*=category]").mapToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadChapters(chapterUrl: String?): List<MangaChapter> {
|
||||
if (chapterUrl.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(chapterUrl.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.select("#dropdownMenuOffset+ul li")
|
||||
.mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val url = a.attrAsRelativeUrl("href")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = a.text(),
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val chapterSlug = Regex("""var chapter_slug = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val mangaSlug = Regex("""var oeuvre_slug = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val cdn = Regex("""var chapter_server = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val cdnDomain = cdn + domain.removePrefix("www")
|
||||
return Regex(""""page_image":"([^"]*)"""").findAll(doc.toString()).asIterable().map {
|
||||
val url =
|
||||
"https://" + cdnDomain + "/uploads/manga/" + mangaSlug + "/chapters_en/" + chapterSlug + "/" + it.groupValues[1]
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/manga-list/").parseHtml()
|
||||
return doc.select("ul li a.category").mapNotNullToSet { a ->
|
||||
val name = a.text()
|
||||
val key = name.lowercase().replace(" ", "-").replace("é", "e").replace("è", "e")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
package org.koitharu.kotatsu.parsers.site.fr
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
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.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("MANGAKAWAII", "MangaKawaii Fr", "fr")
|
||||
internal class MangaKawaii(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAKAWAII, 50) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("www.mangakawaii.io")
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("Accept-Language", "fr")
|
||||
.build()
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?query=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&search_type=manga&page=")
|
||||
append(page)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.sortOrder == SortOrder.UPDATED && filter.tags.isNotEmpty()) {
|
||||
throw IllegalArgumentException("Filtrer part tag n'est pas disponible avec le tri pas mis à jour")
|
||||
}
|
||||
|
||||
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/manga-list")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/category/")
|
||||
append(it.key)
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("li.section__list-group-item").ifEmpty {
|
||||
doc.select("div.media-thumbnail")
|
||||
}.map { div ->
|
||||
val a = div.selectFirstOrThrow("a")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = (div.selectFirst("img")?.src() ?: a.attr("data-bg")).orEmpty(),
|
||||
title = div.selectFirstOrThrow("h4, .media-thumbnail__name").text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val firstChapter = doc.selectFirst("tr[class*='volume-'] a")?.attr("href")
|
||||
val chaptersDeferred = async { loadChapters(firstChapter) }
|
||||
manga.copy(
|
||||
description = doc.selectFirst("dd.text-justify.text-break")?.text().orEmpty(),
|
||||
altTitle = doc.select("span[itemprop*=alternativeHeadline]").joinToString { ", " },
|
||||
author = doc.select("a[href*=author]").text(),
|
||||
state = when (doc.selectFirst("span.badge.bg-success.text-uppercase")?.text()) {
|
||||
"En Cours" -> MangaState.ONGOING
|
||||
"Terminé" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
tags = doc.select("a[href*=category]").mapToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadChapters(chapterUrl: String?): List<MangaChapter> {
|
||||
if (chapterUrl.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(chapterUrl.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.select("#dropdownMenuOffset+ul li")
|
||||
.mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val url = a.attrAsRelativeUrl("href")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = a.text(),
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val chapterSlug = Regex("""var chapter_slug = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val mangaSlug = Regex("""var oeuvre_slug = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val cdn = Regex("""var chapter_server = "([^"]*)";""").find(doc.toString())?.groupValues?.get(1)
|
||||
val cdnDomain = cdn + domain.removePrefix("www")
|
||||
return Regex(""""page_image":"([^"]*)"""").findAll(doc.toString()).asIterable().map {
|
||||
val url =
|
||||
"https://" + cdnDomain + "/uploads/manga/" + mangaSlug + "/chapters_fr/" + chapterSlug + "/" + it.groupValues[1]
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/manga-list/").parseHtml()
|
||||
return doc.select("ul li a.category").mapNotNullToSet { a ->
|
||||
val name = a.text()
|
||||
val key = name.lowercase().replace(" ", "-").replace("é", "e").replace("è", "e")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,186 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("ADULT_WEBTOON", "AdultWebtoon", "en", ContentType.HENTAI)
|
||||
internal class AdultWebtoon(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.ADULT_WEBTOON, "adultwebtoon.com") {
|
||||
override val tagPrefix = "adult-webtoon-genre/"
|
||||
override val listUrl = "adult-webtoon/"
|
||||
override val postReq = true
|
||||
override val withoutAjax = true
|
||||
|
||||
override val availableStates: Set<MangaState> = emptySet()
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = emptySet()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val pages = page + 1
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=wp-manga")
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append('/')
|
||||
append(tagPrefix)
|
||||
append(it.key)
|
||||
append('/')
|
||||
}
|
||||
} else {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
}
|
||||
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append('/')
|
||||
}
|
||||
|
||||
append("?m_orderby=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
else -> append("latest")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append("/?m_orderby=latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val body = doc.body()
|
||||
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
||||
val desc = body.select(selectDesc).html()
|
||||
val stateDiv = if (selectState.isEmpty()) {
|
||||
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
||||
} else {
|
||||
body.selectFirst(selectState)
|
||||
}
|
||||
|
||||
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
in paused -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val alt =
|
||||
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
||||
?.trim()
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = alt,
|
||||
state = state,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val url = "https://$domain/wp-admin/admin-ajax.php"
|
||||
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 doc = makeRequest(url, postData.toRequestBody(), headers)
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
||||
var retryCount = 0
|
||||
val backoffDelay = 2000L // Initial delay (milliseconds)
|
||||
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
||||
while (true) {
|
||||
try {
|
||||
return context.httpClient.newCall(request).execute().parseHtml()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log or handle the exception as needed
|
||||
if (++retryCount <= 5) {
|
||||
withContext(Dispatchers.Default) {
|
||||
delay(backoffDelay)
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.Broken
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@Broken
|
||||
@MangaSourceParser("AQUAMANGA", "AquaManga", "en")
|
||||
internal class AquaManga(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.AQUAMANGA, "aquareader.net", 20)
|
||||
|
||||
@ -1,13 +1,182 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("HENTAIMANGA", "HentaiManga", "en", ContentType.HENTAI)
|
||||
internal class HentaiManga(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.HENTAIMANGA, "hentaimanga.me", 36) {
|
||||
override val postReq = true
|
||||
override val withoutAjax = true
|
||||
override val availableStates: Set<MangaState> = emptySet()
|
||||
override val availableContentRating: Set<ContentRating> = emptySet()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val pages = page + 1
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=wp-manga")
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append('/')
|
||||
append(tagPrefix)
|
||||
append(it.key)
|
||||
append('/')
|
||||
}
|
||||
} else {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
}
|
||||
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append('/')
|
||||
}
|
||||
|
||||
append("?m_orderby=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
else -> append("latest")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append("/?m_orderby=latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val body = doc.body()
|
||||
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
||||
val desc = body.select(selectDesc).html()
|
||||
val stateDiv = if (selectState.isEmpty()) {
|
||||
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
||||
} else {
|
||||
body.selectFirst(selectState)
|
||||
}
|
||||
|
||||
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
in paused -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val alt =
|
||||
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
||||
?.trim()
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = alt,
|
||||
state = state,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val url = "https://$domain/wp-admin/admin-ajax.php"
|
||||
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 doc = makeRequest(url, postData.toRequestBody(), headers)
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
||||
var retryCount = 0
|
||||
val backoffDelay = 2000L // Initial delay (milliseconds)
|
||||
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
||||
while (true) {
|
||||
try {
|
||||
return context.httpClient.newCall(request).execute().parseHtml()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log or handle the exception as needed
|
||||
if (++retryCount <= 5) {
|
||||
withContext(Dispatchers.Default) {
|
||||
delay(backoffDelay)
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,182 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("HENTAIWEBTOON", "HentaiWebtoon", "en", ContentType.HENTAI)
|
||||
internal class HentaiWebtoon(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.HENTAIWEBTOON, "hentaiwebtoon.com") {
|
||||
override val postReq = true
|
||||
override val withoutAjax = true
|
||||
override val availableStates: Set<MangaState> = emptySet()
|
||||
override val availableContentRating: Set<ContentRating> = emptySet()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val pages = page + 1
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=wp-manga")
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append('/')
|
||||
append(tagPrefix)
|
||||
append(it.key)
|
||||
append('/')
|
||||
}
|
||||
} else {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
}
|
||||
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append('/')
|
||||
}
|
||||
|
||||
append("?m_orderby=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
else -> append("latest")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append("/?m_orderby=latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val body = doc.body()
|
||||
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
||||
val desc = body.select(selectDesc).html()
|
||||
val stateDiv = if (selectState.isEmpty()) {
|
||||
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
||||
} else {
|
||||
body.selectFirst(selectState)
|
||||
}
|
||||
|
||||
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
in paused -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val alt =
|
||||
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
||||
?.trim()
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = alt,
|
||||
state = state,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val url = "https://$domain/wp-admin/admin-ajax.php"
|
||||
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 doc = makeRequest(url, postData.toRequestBody(), headers)
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
||||
var retryCount = 0
|
||||
val backoffDelay = 2000L // Initial delay (milliseconds)
|
||||
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
||||
while (true) {
|
||||
try {
|
||||
return context.httpClient.newCall(request).execute().parseHtml()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log or handle the exception as needed
|
||||
if (++retryCount <= 5) {
|
||||
withContext(Dispatchers.Default) {
|
||||
delay(backoffDelay)
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,48 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.parseFailed
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("HUNLIGHT", "HunLight", "en")
|
||||
internal class HunLight(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.HUNLIGHT, "hunlight.com")
|
||||
MadaraParser(context, MangaParserSource.HUNLIGHT, "hunlight.com") {
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
val url = mangaUrl.toAbsoluteUrl(domain).removeSuffix('/') + "/ajax/chapters/"
|
||||
val doc = webClient.httpPost(url, emptyMap()).parseHtml()
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select(selectChapter).mapChapters { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,136 +1,16 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.Broken
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@Broken // Redirect to @XMANHWA
|
||||
@MangaSourceParser("INSTAMANHWA", "InstaManhwa", "en", ContentType.HENTAI)
|
||||
internal class InstaManhwa(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.INSTAMANHWA, "www.instamanhwa.com", 15) {
|
||||
override val tagPrefix = "genre/"
|
||||
override val listUrl = "latest/"
|
||||
override val postReq = true
|
||||
override val datePattern = "d MMMM, yyyy"
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
override val availableStates: Set<MangaState> get() = emptySet()
|
||||
override val availableContentRating: Set<ContentRating> = emptySet()
|
||||
|
||||
init {
|
||||
paginator.firstPage = 1
|
||||
searchPaginator.firstPage = 1
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("/genre/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
} else {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> append("/latest")
|
||||
SortOrder.NEWEST -> append("/new")
|
||||
SortOrder.ALPHABETICAL -> append("/alphabet")
|
||||
else -> append("/latest")
|
||||
}
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/latest?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.page-listing-item div.page-item-detail").ifEmpty {
|
||||
doc.select("div.page-item-detail.manga")
|
||||
}.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = div.selectFirst("img")?.src().orEmpty(),
|
||||
title = (summary?.selectFirst("h3") ?: summary?.selectFirst("h4"))?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = div.selectFirst("span.total_votes")?.ownText()?.toFloatOrNull()?.div(5f) ?: -1f,
|
||||
tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text().ifEmpty { return@mapNotNullToSet null }.toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
}.orEmpty(),
|
||||
author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(),
|
||||
state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText()?.trim()
|
||||
?.lowercase()) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed " -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val token = document.select("meta")[2].attr("content")
|
||||
val url = "https://$domain/ajax"
|
||||
val postData = "_token=$token&action=manga_get_chapters&manga=$mangaId"
|
||||
val doc = webClient.httpPost(url, postData).parseHtml()
|
||||
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
|
||||
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
MadaraParser(context, MangaParserSource.INSTAMANHWA, "www.xmanhwa.me", 15) {
|
||||
override val sourceLocale: Locale = Locale.ENGLISH
|
||||
override val selectPage = "img"
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
// need to login and pay for read
|
||||
@MangaSourceParser("KIARA18", "Kiara18", "en", ContentType.HENTAI)
|
||||
internal class Kiara18(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.KIARA18, "18.kiara.cool")
|
||||
@ -0,0 +1,15 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("MADARADEX", "MadaraDex", "en", ContentType.HENTAI)
|
||||
internal class MadaraDex(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.MADARADEX, "madaradex.org") {
|
||||
override val listUrl = "title/"
|
||||
override val tagPrefix = "genre/"
|
||||
override val postReq = true
|
||||
}
|
||||
@ -1,13 +1,185 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("MANYTOON", "ManyToon", "en", ContentType.HENTAI)
|
||||
internal class ManyToon(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.MANYTOON, "manytoon.com", 20) {
|
||||
override val listUrl = "comic/"
|
||||
override val postReq = true
|
||||
override val withoutAjax = true
|
||||
|
||||
override val availableStates: Set<MangaState> = emptySet()
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = emptySet()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val pages = page + 1
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=wp-manga")
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append('/')
|
||||
append(tagPrefix)
|
||||
append(it.key)
|
||||
append('/')
|
||||
}
|
||||
} else {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
}
|
||||
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append('/')
|
||||
}
|
||||
|
||||
append("?m_orderby=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
else -> append("latest")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append('/')
|
||||
append(listUrl)
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append("/?m_orderby=latest")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val body = doc.body()
|
||||
val chaptersDeferred = async { loadChapters(manga.url, doc) }
|
||||
val desc = body.select(selectDesc).html()
|
||||
val stateDiv = if (selectState.isEmpty()) {
|
||||
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
|
||||
} else {
|
||||
body.selectFirst(selectState)
|
||||
}
|
||||
|
||||
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
in paused -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val alt =
|
||||
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
|
||||
?.trim()
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||
title = a.text().toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
description = desc,
|
||||
altTitle = alt,
|
||||
state = state,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val url = "https://$domain/wp-admin/admin-ajax.php"
|
||||
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 doc = makeRequest(url, postData.toRequestBody(), headers)
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylePage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link,
|
||||
name = name,
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
|
||||
var retryCount = 0
|
||||
val backoffDelay = 2000L // Initial delay (milliseconds)
|
||||
val request = Request.Builder().url(url).post(payload).headers(headers).build()
|
||||
while (true) {
|
||||
try {
|
||||
return context.httpClient.newCall(request).execute().parseHtml()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Log or handle the exception as needed
|
||||
if (++retryCount <= 5) {
|
||||
withContext(Dispatchers.Default) {
|
||||
delay(backoffDelay)
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("ZINCHANMANGA_NET", "ZinchanManga.net", "en")
|
||||
internal class ZinchanMangaNet(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.ZINCHANMANGA_NET, "zinchanmanga.net", 10)
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.tr
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("STRAYFANSUB", "StrayFansub", "tr")
|
||||
internal class StrayFansub(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.STRAYFANSUB, "strayfansub.com", 16) {
|
||||
override val tagPrefix = "seri-turu/"
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("DEXHENTAI", "DexHentai", "en", ContentType.HENTAI)
|
||||
internal class DexHentai(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.DEXHENTAI, "dexhentai.com", 40, 36)
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("ENRYUMANGA", "EnryuManga", "en")
|
||||
internal class EnryuManga(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.ENRYUMANGA, "enryumanga.net", pageSize = 20, searchPageSize = 10) {
|
||||
override val isTagsExclusionSupported = false
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("SNOWSCANS", "SnowScans", "en")
|
||||
internal class SnowScans(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.SNOWSCANS, "snowscans.com", pageSize = 20, searchPageSize = 10) {
|
||||
override val listUrl = "/series"
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("VARNASCAN", "VarnaScan", "en")
|
||||
internal class VarnaScan(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.VARNASCAN, "varnascan.com", pageSize = 20, searchPageSize = 10)
|
||||
@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("VOIDSCANS_CO", "VoidScans", "en")
|
||||
internal class VoidScansCo(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.VOIDSCANS_CO, "voidscans.co", pageSize = 30, searchPageSize = 42)
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("YDCOMICS", "YdComics", "en")
|
||||
internal class YdComics(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.YDCOMICS, "yd-comics.com", pageSize = 20, searchPageSize = 10) {
|
||||
override val listUrl = "/index.php/series"
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.fr
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
import java.util.Locale
|
||||
|
||||
@MangaSourceParser("JAPSCANSFR", "JapScans.fr", "fr")
|
||||
internal class JapScansFR(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.JAPSCANSFR, "japscans.fr", pageSize = 20, searchPageSize = 10) {
|
||||
override val sourceLocale: Locale = Locale.ENGLISH
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.id
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("MANHWALIST_ORG", "ManhwaList.org", "id")
|
||||
internal class ManhwaListOrg(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.MANHWALIST_ORG, "manhwalist.org", pageSize = 20, searchPageSize = 10) {
|
||||
override val sourceLocale: Locale = Locale.ENGLISH
|
||||
override val listUrl = "/manhwa"
|
||||
override val isTagsExclusionSupported = false
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.mangareader.tr
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||
|
||||
@MangaSourceParser("STRAYFANSUB", "StrayFansub", "tr", ContentType.HENTAI)
|
||||
internal class StrayFansub(context: MangaLoaderContext) :
|
||||
MangaReaderParser(context, MangaParserSource.STRAYFANSUB, "strayfansub.com", pageSize = 20, searchPageSize = 10)
|
||||
Loading…
Reference in New Issue