Add XManhwa, MangaMana, MangaFr

Fix XoxoComics close #928
master
devi 2 years ago
parent d774935a6a
commit 8f851282b4

@ -0,0 +1,336 @@
package org.koitharu.kotatsu.parsers.site.fr
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
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.json.JSONArray
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
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 org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGAMANA", "MangaMana", "fr")
internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAMANA, 25) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(
SortOrder.UPDATED,
SortOrder.RATING,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.NEWEST,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("www.manga-mana.com")
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val postData = buildString {
append("page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val domainCdn = "cdn" + domain.removePrefix("www")
val json = webClient.httpGet("https://$domain/search-live?q=${filter.query}").parseJsonArray()
return json.mapJSON { jo ->
val slug = jo.getString("slug") ?: throw Exception("Missing Slug")
val url = "https://$domain/m/$slug"
val img = "https://$domainCdn/uploads/manga/$slug/cover/cover_thumb.jpg"
Manga(
id = generateUid(url),
title = jo.getString("name").orEmpty(),
coverUrl = img,
altTitle = jo.getString("otherNames").orEmpty(),
author = null,
isNsfw = when (jo.getIntOrDefault("caution", 0)) {
0 -> false
2 -> true
else -> false
},
rating = RATING_UNKNOWN,
url = url,
description = jo.getString("summary_old").orEmpty(),
publicUrl = url,
tags = emptySet(),
state = when (jo.getIntOrDefault("status_id_fr", 4)) {
1 -> MangaState.ONGOING
2 -> MangaState.FINISHED
3 -> MangaState.ABANDONED
else -> null
},
source = source,
)
}
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.UPDATED) {
if (filter.tags.isNotEmpty() or filter.states.isNotEmpty()) {
throw IllegalArgumentException("Le filtrage par « tri par : mis à jour » avec les genres ou les statuts n'est pas pris en charge par cette source.")
}
val doc = webClient.httpGet("https://$domain/?page=$page").parseHtml()
return doc.select("div.row div.col_home").map { div ->
val href = div.selectFirstOrThrow("h4 a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
div.selectFirst("img")?.attr("data-src")?.replace(" ", "")
}
Manga(
id = generateUid(href),
title = div.select("h4").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = isNsfw,
coverUrl = img.orEmpty(),
description = null,
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
} else {
filter.tags.oneOrThrowIfMany()?.let {
append("&category=")
append(it.key)
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
append("&sort_by=")
when (filter.sortOrder) {
SortOrder.RATING -> append("score&sort_dir=desc")
SortOrder.NEWEST -> append("updated_at&sort_dir=desc")
SortOrder.ALPHABETICAL -> append("name&sort_dir=asc")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_dir=desc")
else -> append("updated_at&sort_dir=desc")
}
}
}
null -> append("&sort_by=updated_at&sort_dir=desc")
}
}
val url = "https://$domain/liste-mangas"
val token = webClient.httpGet(url).parseHtml().selectFirstOrThrow("meta[name=csrf-token]").attr("content")
val headers = Headers.Builder().add("X-CSRF-TOKEN", token).add("X-Requested-With", "XMLHttpRequest")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()
val doc = makeRequest(url, postData.toRequestBody(), headers)
return doc.select("div.p-2 div.col").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
div.selectFirst("img")?.attr("data-src")?.replace(" ", "")
}
Manga(
id = generateUid(href),
title = div.select("h2.fs-6").text(),
altTitle = doc.selectFirst(".mangalist_item_othernames")?.text().orEmpty(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = div.getElementById("avgrating")?.ownText()?.toFloat()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = isNsfw,
coverUrl = img.orEmpty(),
description = div.selectFirst(".mangalist_item_description")?.text().orEmpty(),
tags = div.select("div.mb-1 a").mapNotNullToSet {
val key = it.attr("href").substringAfterLast('=')
MangaTag(
key = key,
title = it.text(),
source = source,
)
},
state = null,
author = 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 Jsoup.parse(context.httpClient.newCall(request).execute().parseJson().getString("html"))
} catch (e: Exception) {
// Log or handle the exception as needed
if (++retryCount <= 5) {
withContext(Dispatchers.Default) {
delay(backoffDelay)
}
} else {
throw e
}
}
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val mangaUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(mangaUrl).parseHtml()
val maxPageChapterSelect = doc.select("ul.pagination a.page-link")
var maxPageChapter = 1
if (!maxPageChapterSelect.isNullOrEmpty()) {
maxPageChapterSelect.map {
val i = it.attr("href").substringAfterLast("=").toInt()
if (i > maxPageChapter) {
maxPageChapter = i
}
}
}
manga.copy(
state = when (doc.select("div.show_details div.d-flex:contains(Statut) span").text()) {
"En Cours" -> MangaState.ONGOING
"Terminé" -> MangaState.FINISHED
"Abandonné" -> MangaState.ABANDONED
else -> null
},
author = doc.selectFirst("div.show_details span[itemprop=author]")?.text().orEmpty(),
description = doc.selectFirst("dd[itemprop=description]")?.text(),
rating = doc.getElementById("avgrating")?.ownText()?.toFloat()?.div(5f) ?: RATING_UNKNOWN,
tags = doc.select("ul.list-unstyled li a.category").mapNotNullToSet {
val key = it.attr("href").substringAfterLast('=')
MangaTag(
key = key,
title = it.text(),
source = source,
)
},
chapters = run {
if (maxPageChapter == 1) {
parseChapters(doc)
} else {
coroutineScope {
val result = ArrayList(parseChapters(doc))
result.ensureCapacity(result.size * maxPageChapter)
(2..maxPageChapter).map { i ->
async {
loadChapters(mangaUrl, i)
}
}.awaitAll()
.flattenTo(result)
result
}
}
}.reversed(),
)
}
private suspend fun loadChapters(baseUrl: String, page: Int): List<MangaChapter> {
return parseChapters(webClient.httpGet("$baseUrl?page=$page").parseHtml().body())
}
private val dateFormat = SimpleDateFormat("d MMM yyyy", sourceLocale)
private fun parseChapters(doc: Element): List<MangaChapter> {
return doc.select("ul.list-unstyled li a.chapter_link")
.mapChapters { i, a ->
val href = a.attrAsRelativeUrl("href")
val name = a.selectFirst(".chapter div")?.html()?.substringBefore("<") ?: "Chapitre $i"
val dateText = a.selectFirst(".small")?.text()
val chapterN = href.substringAfterLast('/').replace("-", ".").replace("[^0-9.]".toRegex(), "").toFloat()
MangaChapter(
id = generateUid(href),
name = name,
number = chapterN,
volume = 0,
url = href,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val docs = webClient.httpGet(chapterUrl).parseHtml()
val cdn = docs.selectFirstOrThrow("script:containsData(var cdn = )").data().substringAfterLast("var cdn = \"")
.substringBefore('"')
val domainCdn = cdn + domain.removePrefix("www")
val slugManga = chapterUrl.substringAfter("/m/").substringBeforeLast('/')
val slugChapter = chapterUrl.substringAfterLast('/')
val script = docs.selectFirstOrThrow("script:containsData(var pages =)")
val json = JSONArray(script.data().substringAfter("pages = ").substringBefore("; var next_chapter"))
val pages = ArrayList<MangaPage>(json.length())
for (i in 0 until json.length()) {
val img = json.getJSONObject(i).getString("image")
val v = json.getJSONObject(i).getInt("version")
val url = "https://$domainCdn/uploads/manga/$slugManga/chapters_fr/$slugChapter/$img?$v"
pages.add(
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
),
)
}
return pages
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/liste-mangas").parseHtml()
return doc.select("select.selectpicker option").drop(1).mapNotNullToSet {
MangaTag(
key = it.attr("value"),
title = it.text(),
source = source,
)
}
}
}

@ -0,0 +1,15 @@
package org.koitharu.kotatsu.parsers.site.madara.id
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
import java.util.Locale
@MangaSourceParser("XMANHWA", "XManhwa", "id", ContentType.HENTAI)
internal class XManhwa(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.XMANHWA, "www.xmanhwa.me", 10) {
override val sourceLocale: Locale = Locale.ENGLISH
override val selectPage = "img"
}

@ -3,11 +3,13 @@ package org.koitharu.kotatsu.parsers.site.scan
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jsoup.Jsoup
import org.koitharu.kotatsu.parsers.MangaLoaderContext
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 org.koitharu.kotatsu.parsers.util.json.unescapeJson
import java.text.SimpleDateFormat
import java.util.*
@ -22,6 +24,8 @@ internal abstract class ScanParser(
EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING)
override val configKeyDomain = ConfigKey.Domain(domain)
protected open val listUrl = "/manga"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
var query = false
@ -38,7 +42,7 @@ internal abstract class ScanParser(
is MangaListFilter.Advanced -> {
append("/manga")
append(listUrl)
append("?q=")
append(
when (filter.sortOrder) {
@ -60,62 +64,40 @@ internal abstract class ScanParser(
}
null -> {
append("/manga?page=")
append(listUrl)
append("?page=")
append(page.toString())
}
}
}
if (query) {
val doc = webClient.httpGet(url).parseRaw()
val list = if (doc.contains("grid-item-series")) {
doc.split("grid-item-series").drop(1)
} else {
doc.split("class=\\u0022series\\u0022\\").drop(1)
}
return list.map { l ->
val href = l.substringAfter("href=\\u0022\\").substringBefore("\\u0022").replace("\\", "")
val cover = l.substringAfter("data-src=\\u0022").substringBefore("\\u0022\\u003E").replace("\\", "")
val title = l.substringAfter("item-title\\u0022\\u003E").substringBefore("\\u003C\\/p\\u003E").ifEmpty {
l.substringAfter("\\u003Ch3\\u003E").substringBefore("\\u003C\\/h3\\u003E")
}
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = cover,
title = title,
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
val doc = if (query) {
val raw = webClient.httpGet(url).parseRaw()
Jsoup.parseBodyFragment(
raw.unescapeJson(),
domain,
)
} else {
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-paginated .series, .series-paginated .grid-item-series").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.attr("data-src")?.replace("\t", "").orEmpty(),
title = div.selectFirstOrThrow(".link-series h3, .item-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
webClient.httpGet(url).parseHtml()
}
return doc.select(".series, .series-paginated .grid-item-series").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.attr("data-src")?.replace("\t", "").orEmpty(),
title = div.selectFirstOrThrow(".link-series h3, .item-title").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
@ -130,7 +112,7 @@ internal abstract class ScanParser(
protected suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()
val tagElements = webClient.httpGet("https://$domain/manga").parseHtml()
val tagElements = webClient.httpGet("https://$domain$listUrl").parseHtml()
.requireElementById("filter-wrapper")
.select(".form-filters div.form-check, .form-filters div.custom-control")
for (el in tagElements) {

@ -0,0 +1,50 @@
package org.koitharu.kotatsu.parsers.site.scan.fr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.site.scan.ScanParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("MANGAFR", "MangaFr", "fr")
internal class MangaFr(context: MangaLoaderContext) :
ScanParser(context, MangaParserSource.MANGAFR, "www.mangafr.org") {
override val listUrl = "/series"
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("MM-dd-yyyy", sourceLocale)
return manga.copy(
rating = doc.selectFirst(".card-series-detail .rate-value span, .card-series-about .rate-value span")
?.ownText()?.toFloatOrNull()?.div(5f)
?: RATING_UNKNOWN,
tags = emptySet(),
author = doc.selectFirst(".card-series-detail .col-6:contains(Autore) div, .card-series-about .mb-3:contains(Autore) a")
?.text(),
altTitle = doc.selectFirst(".card div.col-12.mb-4 h2, .card-series-about .h6")?.text().orEmpty(),
description = doc.selectFirst(".card div.col-12.mb-4 p, .card-series-desc .mb-4 p")?.html().orEmpty(),
chapters = doc.select(".chapters-list .col-chapter, .card-list-chapter .col-chapter")
.mapChapters(reversed = true) { i, div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
MangaChapter(
id = generateUid(href),
name = div.selectFirstOrThrow("h5").html().substringBefore("<div").substringAfter("</span>"),
number = i + 1f,
volume = 0,
url = href,
scanlator = null,
uploadDate = dateFormat.tryParse(doc.selectFirstOrThrow("h5 div").text()),
branch = null,
source = source,
)
},
)
}
}

@ -1,14 +1,13 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.en
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS)
@ -119,7 +118,7 @@ internal class XoxoComics(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) }
val chaptersDeferred = async { loadChapters(fullUrl) }
val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let {
@ -146,29 +145,37 @@ internal class XoxoComics(context: MangaLoaderContext) :
)
}
override suspend fun getChapters(doc: Document): List<MangaChapter> {
val pages = doc.select("ul.pagination > li:not(.active)")
return if (pages.size <= 1) {
super.getChapters(doc)
} else {
val subPageChapterList = coroutineScope {
pages.mapNotNull { page ->
val a = page.selectFirst("a") ?: return@mapNotNull null
if (a.text().isNumeric()) {
val href = a.attrAsAbsoluteUrl("href")
async {
super.getChapters(webClient.httpGet(href).parseHtml()).asReversed()
}
} else {
null // TODO support pagination with overflow
}
}.awaitAll().flatten()
}
val firstPageChapterList = super.getChapters(doc).asReversed().toMutableList()
firstPageChapterList.addAll(subPageChapterList)
firstPageChapterList.reverse()
firstPageChapterList.mapIndexed { i, x -> x.copy(volume = x.volume, number = (i + 1).toFloat()) }
private val dateFormat = SimpleDateFormat("MM/dd/yyyy", sourceLocale)
private suspend fun loadChapters(baseUrl: String): List<MangaChapter> {
val chapters = ArrayList<MangaChapter>()
var page = 0
while (true) {
++page
val doc = webClient.httpGet("$baseUrl?page=$page").parseHtml()
doc.selectFirst("#nt_listchapter nav ul li:not(.heading)") ?: break
chapters.addAll(
doc.select("#nt_listchapter nav ul li:not(.heading)").mapChapters { _, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attr("href")
val dateText = li.selectFirst("div.col-xs-3")?.text()
MangaChapter(
id = generateUid(href),
name = a.text(),
number = 0f,
volume = 0,
url = href,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
},
)
}
chapters.reverse()
return chapters.mapIndexed { i, x -> x.copy(volume = x.volume, number = (i + 1).toFloat()) }
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {

Loading…
Cancel
Save