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

@ -31,23 +31,29 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
filter: MangaListFilter?,
): List<Manga> {
if (!query.isNullOrEmpty()) {
val filters =
when (filter) {
is MangaListFilter.Search -> {
return if (offset == 0) {
search(query)
search(filter.query)
} else {
emptyList()
}
}
val filters = tags?.takeUnless { it.isEmpty() }?.joinToString(
is MangaListFilter.Advanced -> {
filter.tags.takeUnless { it.isEmpty() }?.joinToString(
separator = ",",
prefix = "genres: [",
postfix = "]",
) { "\"${it.key}\"" }.orEmpty()
}
null -> ""
}
val array = apiCall(
"""
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 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> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chapterProtector = doc.getElementById("chapter-protector-data")
if (chapterProtector == null) {
val root = doc.body().selectFirst(selectBodyPage)
?: throw ParseException("No image found, try to log in", fullUrl)
throw if (doc.selectFirst(selectRequiredLogin) != null) {
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 ->
val img = div.selectFirstOrThrow("img")
val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found")
@ -594,6 +600,7 @@ internal abstract class MadaraParser(
source = source,
)
}
}
} else {
val chapterProtectorHtml = chapterProtector.attr("src")

@ -14,7 +14,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("ROCKSMANGA", "RocksManga", "ar")
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 datePattern = "d MMMM yyyy"
override val selectDate = ".ch-post-time"

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

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGAHALL", "MangaHall", "en", ContentType.HENTAI)
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
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
}
}
}
}
}

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("RIO2MANGANET", "ZinchanManga", "en")
@MangaSourceParser("RIO2MANGANET", "ZinchanManga.mobi", "en")
internal class Rio2MangaNet(context: MangaLoaderContext) :
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.site.madara.MadaraParser
@MangaSourceParser("ZINCHANMANGA", "ZinChanManga", "en", ContentType.HENTAI)
@MangaSourceParser("ZINCHANMANGA", "ZinChanManga.com", "en", ContentType.HENTAI)
internal class ZinChanManga(context: MangaLoaderContext) :
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)
internal class Fbsquads(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.FBSQUADS, "fbsscan.com") {
MadaraParser(context, MangaParserSource.FBSQUADS, "fbsquadx.com") {
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)
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 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")
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 {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("NORMOYUN", "MaxLevelTeam", "ar")
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 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")
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"
}

@ -9,4 +9,4 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@Broken
@MangaSourceParser("MANHWA_FREAK", "ManhwaFreak", "en")
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
@MangaSourceParser("ALCEASCAN", "AlceaScan", "id")
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
}

@ -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 java.util.*
@MangaSourceParser("MANHWALIST", "ManhwaList", "id")
@MangaSourceParser("MANHWALIST", "ManhwaList.com", "id")
internal class ManhwalistParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaParserSource.MANHWALIST, "manhwalist.com", pageSize = 24, searchPageSize = 10) {
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.site.mmrcms.MmrcmsParser
@MangaSourceParser("READCOMICSONLINE", "ReadComicsOnline", "en", ContentType.COMICS)
@MangaSourceParser("READCOMICSONLINE", "ReadComicsOnline.ru", "en", ContentType.COMICS)
internal class ReadComicsOnline(context: MangaLoaderContext) :
MmrcmsParser(context, MangaParserSource.READCOMICSONLINE, "readcomicsonline.ru") {
override val selectState = "dt:contains(Status)"

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

Loading…
Cancel
Save