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() )
devi 2 years ago
parent 71affd155c
commit 1f7fe2aed3

@ -31,23 +31,29 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
override suspend fun getList( override suspend fun getList(
offset: Int, offset: Int,
query: String?, filter: MangaListFilter?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
if (!query.isNullOrEmpty()) { val filters =
when (filter) {
is MangaListFilter.Search -> {
return if (offset == 0) { return if (offset == 0) {
search(query) search(filter.query)
} else { } else {
emptyList() emptyList()
} }
} }
val filters = tags?.takeUnless { it.isEmpty() }?.joinToString(
is MangaListFilter.Advanced -> {
filter.tags.takeUnless { it.isEmpty() }?.joinToString(
separator = ",", separator = ",",
prefix = "genres: [", prefix = "genres: [",
postfix = "]", postfix = "]",
) { "\"${it.key}\"" }.orEmpty() ) { "\"${it.key}\"" }.orEmpty()
}
null -> ""
}
val array = apiCall( val array = apiCall(
""" """
getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) { getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) {

@ -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,
)
}
}
}

@ -575,15 +575,21 @@ internal abstract class MadaraParser(
} }
protected open val selectBodyPage = "div.main-col-inner div.reading-content" protected open val selectBodyPage = "div.main-col-inner div.reading-content"
protected open val selectPage = "div.page-break, div.login-required" protected open val selectPage = "div.page-break"
protected open val selectRequiredLogin = ".content-blocked, .login-required"
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain) val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val chapterProtector = doc.getElementById("chapter-protector-data") val chapterProtector = doc.getElementById("chapter-protector-data")
if (chapterProtector == null) { if (chapterProtector == null) {
val root = doc.body().selectFirst(selectBodyPage) throw if (doc.selectFirst(selectRequiredLogin) != null) {
?: throw ParseException("No image found, try to log in", fullUrl) AuthRequiredException(source)
} else {
val root = doc.body().selectFirst(selectBodyPage) ?: throw ParseException(
"No image found, try to log in",
fullUrl,
)
return root.select(selectPage).map { div -> return root.select(selectPage).map { div ->
val img = div.selectFirstOrThrow("img") val img = div.selectFirstOrThrow("img")
val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found") val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found")
@ -594,6 +600,7 @@ internal abstract class MadaraParser(
source = source, source = source,
) )
} }
}
} else { } else {
val chapterProtectorHtml = chapterProtector.attr("src") val chapterProtectorHtml = chapterProtector.attr("src")

@ -14,7 +14,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("ROCKSMANGA", "RocksManga", "ar") @MangaSourceParser("ROCKSMANGA", "RocksManga", "ar")
internal class RocksManga(context: MangaLoaderContext) : internal class RocksManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.ROCKSMANGA, "rocks-manga.com") { MadaraParser(context, MangaParserSource.ROCKSMANGA, "rocksmanga.com") {
override val selectChapter = "ul#chapter-list li.chapter-item" override val selectChapter = "ul#chapter-list li.chapter-item"
override val datePattern = "d MMMM yyyy" override val datePattern = "d MMMM yyyy"
override val selectDate = ".ch-post-time" override val selectDate = ".ch-post-time"

@ -1,14 +1,186 @@
package org.koitharu.kotatsu.parsers.site.madara.en 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser 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) @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 postReq = true 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 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@MangaSourceParser("AQUAMANGA", "AquaManga", "en") @MangaSourceParser("AQUAMANGA", "AquaManga", "en")
internal class AquaManga(context: MangaLoaderContext) : internal class AquaManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.AQUAMANGA, "aquareader.net", 20) MadaraParser(context, MangaParserSource.AQUAMANGA, "aquareader.net", 20)

@ -1,13 +1,182 @@
package org.koitharu.kotatsu.parsers.site.madara.en 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("HENTAIMANGA", "HentaiManga", "en", ContentType.HENTAI) @MangaSourceParser("HENTAIMANGA", "HentaiManga", "en", ContentType.HENTAI)
internal class HentaiManga(context: MangaLoaderContext) : internal class HentaiManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HENTAIMANGA, "hentaimanga.me", 36) { MadaraParser(context, MangaParserSource.HENTAIMANGA, "hentaimanga.me", 36) {
override val postReq = true 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 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("HENTAIWEBTOON", "HentaiWebtoon", "en", ContentType.HENTAI) @MangaSourceParser("HENTAIWEBTOON", "HentaiWebtoon", "en", ContentType.HENTAI)
internal class HentaiWebtoon(context: MangaLoaderContext) : internal class HentaiWebtoon(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HENTAIWEBTOON, "hentaiwebtoon.com") { MadaraParser(context, MangaParserSource.HENTAIWEBTOON, "hentaiwebtoon.com") {
override val postReq = true 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 package org.koitharu.kotatsu.parsers.site.madara.en
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser 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.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser 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") @MangaSourceParser("HUNLIGHT", "HunLight", "en")
internal class HunLight(context: MangaLoaderContext) : 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 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* 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 java.text.SimpleDateFormat
import java.util.* import java.util.*
@Broken // Redirect to @XMANHWA
@MangaSourceParser("INSTAMANHWA", "InstaManhwa", "en", ContentType.HENTAI) @MangaSourceParser("INSTAMANHWA", "InstaManhwa", "en", ContentType.HENTAI)
internal class InstaManhwa(context: MangaLoaderContext) : internal class InstaManhwa(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.INSTAMANHWA, "www.instamanhwa.com", 15) { MadaraParser(context, MangaParserSource.INSTAMANHWA, "www.xmanhwa.me", 15) {
override val tagPrefix = "genre/" override val sourceLocale: Locale = Locale.ENGLISH
override val listUrl = "latest/" override val selectPage = "img"
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,
)
}
}
} }

@ -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
}

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGAHALL", "MangaHall", "en", ContentType.HENTAI) @MangaSourceParser("MANGAHALL", "MangaHall", "en", ContentType.HENTAI)
internal class MangaHall(context: MangaLoaderContext) : internal class MangaHall(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANGAHALL, "mangahall.net", 24) MadaraParser(context, MangaParserSource.MANGAHALL, "mangahall.org", 24)

@ -1,13 +1,185 @@
package org.koitharu.kotatsu.parsers.site.madara.en 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.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("MANYTOON", "ManyToon", "en", ContentType.HENTAI) @MangaSourceParser("MANYTOON", "ManyToon", "en", ContentType.HENTAI)
internal class ManyToon(context: MangaLoaderContext) : internal class ManyToon(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANYTOON, "manytoon.com", 20) { MadaraParser(context, MangaParserSource.MANYTOON, "manytoon.com", 20) {
override val listUrl = "comic/" 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
}
}
}
}
} }

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("RIO2MANGANET", "ZinchanManga", "en") @MangaSourceParser("RIO2MANGANET", "ZinchanManga.mobi", "en")
internal class Rio2MangaNet(context: MangaLoaderContext) : internal class Rio2MangaNet(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.RIO2MANGANET, "zinchanmanga.mobi", 10) MadaraParser(context, MangaParserSource.RIO2MANGANET, "zinchanmanga.mobi", 10)

@ -6,6 +6,6 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("ZINCHANMANGA", "ZinChanManga", "en", ContentType.HENTAI) @MangaSourceParser("ZINCHANMANGA", "ZinChanManga.com", "en", ContentType.HENTAI)
internal class ZinChanManga(context: MangaLoaderContext) : internal class ZinChanManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.ZINCHANMANGA, "zinchanmanga.com", 10) MadaraParser(context, MangaParserSource.ZINCHANMANGA, "zinchanmanga.com", 10)

@ -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)

@ -8,6 +8,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("FBSQUADS", "FbSquads", "pt", ContentType.HENTAI) @MangaSourceParser("FBSQUADS", "FbSquads", "pt", ContentType.HENTAI)
internal class Fbsquads(context: MangaLoaderContext) : internal class Fbsquads(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.FBSQUADS, "fbsscan.com") { MadaraParser(context, MangaParserSource.FBSQUADS, "fbsquadx.com") {
override val datePattern: String = "dd/MM/yyyy" override val datePattern: String = "dd/MM/yyyy"
} }

@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("JIANGZAITOON", "JiangzaiToon", "tr", ContentType.HENTAI) @MangaSourceParser("JIANGZAITOON", "JiangzaiToon", "tr", ContentType.HENTAI)
internal class Jiangzaitoon(context: MangaLoaderContext) : internal class Jiangzaitoon(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.JIANGZAITOON, "jiangzaitoon.dev") { MadaraParser(context, MangaParserSource.JIANGZAITOON, "jiangzaitoon.pro") {
override val datePattern = "d MMMM yyyy" override val datePattern = "d MMMM yyyy"
override val postReq = true override val postReq = true
} }

@ -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/"
}

@ -10,7 +10,7 @@ import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("MANJANOON", "Manjanoon", "ar") @MangaSourceParser("MANJANOON", "Manjanoon", "ar")
internal class Manjanoon(context: MangaLoaderContext) : internal class Manjanoon(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.MANJANOON, "manjanoon.co", pageSize = 21, searchPageSize = 10) { MangaReaderParser(context, MangaParserSource.MANJANOON, "noonscan.net", pageSize = 21, searchPageSize = 10) {
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("NORMOYUN", "MaxLevelTeam", "ar") @MangaSourceParser("NORMOYUN", "MaxLevelTeam", "ar")
internal class Normoyun(context: MangaLoaderContext) : internal class Normoyun(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.NORMOYUN, "maxlevelteam.com", pageSize = 42, searchPageSize = 39) { MangaReaderParser(context, MangaParserSource.NORMOYUN, "tatwt.com", pageSize = 42, searchPageSize = 39) {
override val datePattern = "MMMM dd, yyyy" override val datePattern = "MMMM dd, yyyy"
override val selectMangaList = ".listupd .bs .bsx" override val selectMangaList = ".listupd .bs .bsx"

@ -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
}

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("MAGUSMANGA", "Recipeslik", "en") @MangaSourceParser("MAGUSMANGA", "Recipeslik", "en")
internal class MagusManga(context: MangaLoaderContext) : internal class MagusManga(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.MAGUSMANGA, "recipeslik.online", pageSize = 20, searchPageSize = 10) { MangaReaderParser(context, MangaParserSource.MAGUSMANGA, "oocini.biz", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/series" override val listUrl = "/series"
} }

@ -9,4 +9,4 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@Broken @Broken
@MangaSourceParser("MANHWA_FREAK", "ManhwaFreak", "en") @MangaSourceParser("MANHWA_FREAK", "ManhwaFreak", "en")
internal class ManhwaFreak(context: MangaLoaderContext) : internal class ManhwaFreak(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.MANHWA_FREAK, "manhwafreak.site", pageSize = 20, searchPageSize = 10) MangaReaderParser(context, MangaParserSource.MANHWA_FREAK, "manhwafreak.xyz", 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("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
}

@ -10,6 +10,6 @@ import java.util.*
@Broken @Broken
@MangaSourceParser("ALCEASCAN", "AlceaScan", "id") @MangaSourceParser("ALCEASCAN", "AlceaScan", "id")
internal class AlceaScan(context: MangaLoaderContext) : internal class AlceaScan(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.ALCEASCAN, "alceascan.my.id", pageSize = 20, searchPageSize = 10) { MangaReaderParser(context, MangaParserSource.ALCEASCAN, "alceacomic.my.id", pageSize = 20, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH 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
}

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import java.util.* import java.util.*
@MangaSourceParser("MANHWALIST", "ManhwaList", "id") @MangaSourceParser("MANHWALIST", "ManhwaList.com", "id")
internal class ManhwalistParser(context: MangaLoaderContext) : internal class ManhwalistParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.MANHWALIST, "manhwalist.com", pageSize = 24, searchPageSize = 10) { MangaReaderParser(context, MangaParserSource.MANHWALIST, "manhwalist.com", pageSize = 24, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH

@ -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)

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser
@MangaSourceParser("READCOMICSONLINE", "ReadComicsOnline", "en", ContentType.COMICS) @MangaSourceParser("READCOMICSONLINE", "ReadComicsOnline.ru", "en", ContentType.COMICS)
internal class ReadComicsOnline(context: MangaLoaderContext) : internal class ReadComicsOnline(context: MangaLoaderContext) :
MmrcmsParser(context, MangaParserSource.READCOMICSONLINE, "readcomicsonline.ru") { MmrcmsParser(context, MangaParserSource.READCOMICSONLINE, "readcomicsonline.ru") {
override val selectState = "dt:contains(Status)" override val selectState = "dt:contains(Status)"

@ -47,29 +47,41 @@ internal class NudeMoonParser(
override suspend fun getList( override suspend fun getList(
offset: Int, offset: Int,
query: String?, filter: MangaListFilter?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
val domain = domain val domain = domain
val url = when {
!query.isNullOrEmpty() -> { val url =
when (filter) {
is MangaListFilter.Search -> {
if (!isAuthorized) { if (!isAuthorized) {
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
"https://$domain/search?stext=${query.urlEncoded()}&rowstart=$offset" "https://$domain/search?stext=${filter.query.urlEncoded()}&rowstart=$offset"
} }
!tags.isNullOrEmpty() -> tags.joinToString( is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
filter.tags.joinToString(
separator = "_", separator = "_",
prefix = "https://$domain/tags/", prefix = "https://$domain/tags/",
postfix = "&rowstart=$offset", postfix = "&rowstart=$offset",
transform = { it.key.urlEncoded() }, transform = { it.key.urlEncoded() },
) )
} else {
val order = when (filter.sortOrder) {
SortOrder.POPULARITY -> "views"
SortOrder.NEWEST -> "date"
SortOrder.RATING -> "like"
else -> "like"
}
"https://$domain/all_manga?$order&rowstart=$offset"
}
}
else -> "https://$domain/all_manga?${getSortKey(sortOrder)}&rowstart=$offset" null -> "https://$domain/all_manga?views&rowstart=$offset"
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.body().select("table.news_pic2").mapNotNull { row -> return doc.body().select("table.news_pic2").mapNotNull { row ->
val a = row.selectFirstOrThrow("a") val a = row.selectFirstOrThrow("a")
@ -176,11 +188,4 @@ internal class NudeMoonParser(
} }
} }
} }
private fun getSortKey(sortOrder: SortOrder) = when (sortOrder) {
SortOrder.POPULARITY -> "views"
SortOrder.NEWEST -> "date"
SortOrder.RATING -> "like"
else -> "like"
}
} }

@ -56,7 +56,8 @@ class HentaiUkrParser(context: MangaLoaderContext) : MangaParser(context, MangaP
MangaChapter( MangaChapter(
id = generateUid(manga.id), id = generateUid(manga.id),
name = manga.title, name = manga.title,
number = 1, number = 1f,
volume = 0,
url = manga.url, url = manga.url,
scanlator = null, scanlator = null,
uploadDate = date.tryParse(jsonDeferred.await().getString("add_date")), uploadDate = date.tryParse(jsonDeferred.await().getString("add_date")),
@ -69,33 +70,35 @@ class HentaiUkrParser(context: MangaLoaderContext) : MangaParser(context, MangaP
override suspend fun getList( override suspend fun getList(
offset: Int, offset: Int,
query: String?, filter: MangaListFilter?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
// Get all manga // Get all manga
val json = allManga.get().toMutableList() val json = allManga.get().toMutableList()
// Search when (filter) {
if (!query.isNullOrEmpty()) { is MangaListFilter.Search -> {
json.retainAll { item -> json.retainAll { item ->
item.getString("name").contains(query, ignoreCase = true) || item.getString("name").contains(filter.query, ignoreCase = true) ||
item.getStringOrNull("eng_name")?.contains(query, ignoreCase = true) == true || item.getStringOrNull("eng_name")?.contains(filter.query, ignoreCase = true) == true ||
item.getStringOrNull("orig_name")?.contains(query, ignoreCase = true) == true || item.getStringOrNull("orig_name")?.contains(filter.query, ignoreCase = true) == true ||
item.getStringOrNull("author")?.contains(query, ignoreCase = true) == true || item.getStringOrNull("author")?.contains(filter.query, ignoreCase = true) == true ||
item.getStringOrNull("team")?.contains(query, ignoreCase = true) == true item.getStringOrNull("team")?.contains(filter.query, ignoreCase = true) == true
} }
} }
if (!tags.isNullOrEmpty()) { is MangaListFilter.Advanced -> {
val ids = tags.mapToSet { it.key } if (filter.tags.isNotEmpty()) {
val ids = filter.tags.mapToSet { it.key }
json.retainAll { item -> json.retainAll { item ->
item.getJSONArray("tags") item.getJSONArray("tags")
.mapJSON { it.getAsString() } .mapJSON { it.getAsString() }
.any { x -> x in ids } .any { x -> x in ids }
} }
} }
}
null -> {}
}
// Return to app // Return to app
return json.drop(offset).take(PAGE_SIZE).map { jo -> return json.drop(offset).take(PAGE_SIZE).map { jo ->

Loading…
Cancel
Save