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 androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.jsoup.Jsoup
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.unescapeJson
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -22,6 +24,8 @@ internal abstract class ScanParser(
EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING) EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING)
override val configKeyDomain = ConfigKey.Domain(domain) override val configKeyDomain = ConfigKey.Domain(domain)
protected open val listUrl = "/manga"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
var query = false var query = false
@ -38,7 +42,7 @@ internal abstract class ScanParser(
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
append("/manga") append(listUrl)
append("?q=") append("?q=")
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
@ -60,62 +64,40 @@ internal abstract class ScanParser(
} }
null -> { null -> {
append("/manga?page=") append(listUrl)
append("?page=")
append(page.toString()) append(page.toString())
} }
} }
} }
if (query) { val doc = if (query) {
val doc = webClient.httpGet(url).parseRaw() val raw = webClient.httpGet(url).parseRaw()
Jsoup.parseBodyFragment(
val list = if (doc.contains("grid-item-series")) { raw.unescapeJson(),
doc.split("grid-item-series").drop(1) domain,
} 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,
)
}
} else { } else {
val doc = webClient.httpGet(url).parseHtml() 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( return doc.select(".series, .series-paginated .grid-item-series").map { div ->
id = generateUid(href), val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
url = href, Manga(
publicUrl = href.toAbsoluteUrl(div.host ?: domain), id = generateUid(href),
coverUrl = div.selectFirst("img")?.attr("data-src")?.replace("\t", "").orEmpty(), url = href,
title = div.selectFirstOrThrow(".link-series h3, .item-title").text().orEmpty(), publicUrl = href.toAbsoluteUrl(div.host ?: domain),
altTitle = null, coverUrl = div.selectFirst("img")?.attr("data-src")?.replace("\t", "").orEmpty(),
rating = RATING_UNKNOWN, title = div.selectFirstOrThrow(".link-series h3, .item-title").text().orEmpty(),
tags = emptySet(), altTitle = null,
author = null, rating = RATING_UNKNOWN,
state = null, tags = emptySet(),
source = source, author = null,
isNsfw = isNsfwSource, state = null,
) source = source,
} isNsfw = isNsfwSource,
)
} }
} }
@ -130,7 +112,7 @@ internal abstract class ScanParser(
protected suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock { protected suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it } tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>() val tagMap = ArrayMap<String, MangaTag>()
val tagElements = webClient.httpGet("https://$domain/manga").parseHtml() val tagElements = webClient.httpGet("https://$domain$listUrl").parseHtml()
.requireElementById("filter-wrapper") .requireElementById("filter-wrapper")
.select(".form-filters div.form-check, .form-filters div.custom-control") .select(".form-filters div.form-check, .form-filters div.custom-control")
for (el in tagElements) { 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 package org.koitharu.kotatsu.parsers.site.wpcomics.en
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
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.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
@MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS) @MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS)
@ -119,7 +118,7 @@ internal class XoxoComics(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga = coroutineScope { override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain) val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val chaptersDeferred = async { getChapters(doc) } val chaptersDeferred = async { loadChapters(fullUrl) }
val desc = doc.selectFirstOrThrow(selectDesc).html() val desc = doc.selectFirstOrThrow(selectDesc).html()
val stateDiv = doc.selectFirst(selectState) val stateDiv = doc.selectFirst(selectState)
val state = stateDiv?.let { val state = stateDiv?.let {
@ -146,29 +145,37 @@ internal class XoxoComics(context: MangaLoaderContext) :
) )
} }
override suspend fun getChapters(doc: Document): List<MangaChapter> { private val dateFormat = SimpleDateFormat("MM/dd/yyyy", sourceLocale)
val pages = doc.select("ul.pagination > li:not(.active)")
return if (pages.size <= 1) { private suspend fun loadChapters(baseUrl: String): List<MangaChapter> {
super.getChapters(doc) val chapters = ArrayList<MangaChapter>()
} else { var page = 0
val subPageChapterList = coroutineScope { while (true) {
pages.mapNotNull { page -> ++page
val a = page.selectFirst("a") ?: return@mapNotNull null val doc = webClient.httpGet("$baseUrl?page=$page").parseHtml()
if (a.text().isNumeric()) { doc.selectFirst("#nt_listchapter nav ul li:not(.heading)") ?: break
val href = a.attrAsAbsoluteUrl("href") chapters.addAll(
async { doc.select("#nt_listchapter nav ul li:not(.heading)").mapChapters { _, li ->
super.getChapters(webClient.httpGet(href).parseHtml()).asReversed() val a = li.selectFirstOrThrow("a")
} val href = a.attr("href")
} else { val dateText = li.selectFirst("div.col-xs-3")?.text()
null // TODO support pagination with overflow MangaChapter(
} id = generateUid(href),
}.awaitAll().flatten() name = a.text(),
} number = 0f,
val firstPageChapterList = super.getChapters(doc).asReversed().toMutableList() volume = 0,
firstPageChapterList.addAll(subPageChapterList) url = href,
firstPageChapterList.reverse() scanlator = null,
firstPageChapterList.mapIndexed { i, x -> x.copy(volume = x.volume, number = (i + 1).toFloat()) } 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> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {

Loading…
Cancel
Save