Move ReaperComics to HeanCms

Rework heancms based on @NagaYZ's code
master
devi 2 years ago
parent 50194df24d
commit 8d5fc945d4

@ -1,263 +0,0 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.Headers
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
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.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
private const val MAX_RETRY_COUNT = 5
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
internal class ReaperComics(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.REAPERCOMICS, pageSize = 20) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL_DESC,
)
override val configKeyDomain = ConfigKey.Domain("reaperscans.com")
private val userAgentKey = ConfigKey.UserAgent(context.getDefaultUserAgent())
private val baseHeaders: Headers
get() = Headers.Builder().add("User-Agent", config[userAgentKey]).build()
override val headers
get() = getApiHeaders()
private fun getApiHeaders(): Headers {
val userCookie = context.cookieJar.getCookies(domain).find {
it.name == "user"
} ?: return baseHeaders
val jo = JSONObject(userCookie.value.urlDecode())
val accessToken = jo.getStringOrNull("access_token") ?: return baseHeaders
return baseHeaders.newBuilder().add("authorization", "bearer $accessToken").build()
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
if (page > 1) return emptyList()
val url = buildString {
append("https://")
append("api.$domain")
append("/query?page=$page&perPage=9999&series_type=Comic")
when (filter) {
is MangaListFilter.Search -> {
append("&query_string=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
append("&orderBy=")
val order = when (filter.sortOrder) {
SortOrder.UPDATED -> "updated_at"
SortOrder.POPULARITY -> "total_views"
SortOrder.NEWEST -> "created_at"
SortOrder.ALPHABETICAL -> "title"
SortOrder.ALPHABETICAL_DESC -> "title"
else -> "updated_at"
}
append(order)
val sortOrder = if (filter.sortOrder == SortOrder.ALPHABETICAL_DESC) "desc" else "asc"
append("&order=$sortOrder")
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> "All"
},
)
}
if (filter.tags.isNotEmpty()) {
append("&tags_ids=")
append(filter.tags.joinToString(separator = "%") { it.key })
}
}
null -> {
append("&orderBy=updated_at")
append("&order=asc")
append("&adult=true")
append("&status=All")
}
}
}
return parseMangaList(webClient.httpGet(url).parseJson())
}
private fun parseMangaList(response: JSONObject): List<Manga> {
return response.getJSONArray("data").mapJSON { it ->
val id = it.getLong("id")
val url = "/comic/${it.getString("series_slug")}"
val title = it.getString("title")
val thumbnailPath = it.getString("thumbnail")
Manga(
id = id,
url = url,
title = title,
altTitle = it.getString("alternative_names").takeIf { it.isNotBlank() },
publicUrl = url.toAbsoluteUrl(domain),
description = it.getString("description"),
rating = it.getFloatOrDefault("rating", RATING_UNKNOWN) / 5f,
isNsfw = isNsfwSource,
coverUrl = "https://media.reaperscans.com/file/4SRBHm//$thumbnailPath",
tags = emptySet(),
state = when (it.getString("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> null
},
author = null,
source = source,
)
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val scriptContent = doc.select("script").find {
it.data().contains("tags")
}?.data()
if (scriptContent != null) {
val jsonString = scriptContent.substringAfter("push(").substringBeforeLast(")")
val jsonArray = JSONArray(jsonString)
val childrenArray = jsonArray.getString(1)
val tagsString = childrenArray.substringAfter("tags:[").substringBeforeLast("]")
val tagObjects = tagsString.split("},{")
return tagObjects.mapNotNullTo(mutableSetOf()) { tagString ->
val id = tagString.substringAfter("\"id\":").substringBefore(",")
val name = tagString.substringAfter("\"name\":\"").substringBefore("\"")
if (id.isNotEmpty() && name.isNotEmpty()) {
MangaTag(
key = id,
title = name.toTitleCase(sourceLocale),
source = source,
)
} else {
null
}
}
}
return emptySet()
}
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", sourceLocale)
override suspend fun getDetails(manga: Manga): Manga {
val seriesid = manga.id
val url = "https://api.$domain/chapter/query?page=1&perPage=9999&series_id=$seriesid"
val response = makeRequest(url)
val data = response.getJSONArray("data")
return manga.copy(
chapters = data.mapJSONIndexed { index, it ->
val chapterUrl =
"/series/${it.getJSONObject("series").getString("series_slug")}/${it.getString("chapter_slug")}"
MangaChapter(
id = it.getLong("id"),
name = it.getString("chapter_name"),
number = (data.length() - index).toFloat(),
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = parseChapterDate(dateFormat, it.getString("created_at")),
branch = null,
source = source,
)
},
)
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
return try {
dateFormat.tryParse(date)
} catch (e: Exception) {
0L
}
}
private val pageSelector = "div#content div.container img"
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val processedUrls = mutableSetOf<String>()
return doc.select(pageSelector).mapNotNull { img ->
val url = img.attr("data-cfsrc").takeIf { it.isNotBlank() }
?: img.attr("src").takeIf { it.isNotBlank() }
?: img.selectFirst("noscript img")?.attr("src")
?: return@mapNotNull null
val relativeUrl = url.toRelativeUrl(domain)
if (relativeUrl !in processedUrls) {
processedUrls.add(relativeUrl)
MangaPage(
id = generateUid(relativeUrl),
url = relativeUrl,
preview = null,
source = source,
)
} else {
null
}
}
}
private suspend fun makeRequest(url: String): JSONObject {
var retryCount = 0
val backoffDelay = 2000L // Initial delay (milliseconds)
val request = Request.Builder().url(url).headers(headers).build()
while (true) {
try {
val response = context.httpClient.newCall(request).execute().parseJson()
return response
} catch (e: Exception) {
// Log or handle the exception as needed
if (++retryCount <= MAX_RETRY_COUNT) {
withContext(Dispatchers.Default) {
delay(backoffDelay)
}
} else {
throw e
}
}
}
}
}

@ -1,11 +1,16 @@
package org.koitharu.kotatsu.parsers.site.heancms
import org.json.JSONArray
import org.json.JSONObject
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.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
import org.koitharu.kotatsu.parsers.util.json.unescapeJson
import java.text.SimpleDateFormat
import java.util.*
@ -41,6 +46,8 @@ internal abstract class HeanCms(
protected open val apiPath
get() = getDomain("api")
protected open val paramsUpdated = "latest"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
@ -54,7 +61,7 @@ internal abstract class HeanCms(
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
@ -64,18 +71,19 @@ internal abstract class HeanCms(
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.UPDATED -> append("$paramsUpdated&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=desc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
SortOrder.ALPHABETICAL -> append("title&order=asc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=desc")
else -> append("latest&order=desc")
}
append("&series_type=Comic&perPage=12")
append("&series_type=Comic&perPage=20")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
@ -83,32 +91,39 @@ internal abstract class HeanCms(
}
null -> {}
null -> append("status=All&orderBy=$paramsUpdated&order=desc&series_type=Comic&perPage=20")
}
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return parseMangaList(webClient.httpGet(url).parseJson())
}
protected open val cdn = "api.$domain/"
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
private fun parseMangaList(response: JSONObject): List<Manga> {
return response.getJSONArray("data").mapJSON { it ->
val id = it.getLong("id")
val url = "/comic/${it.getString("series_slug")}"
val title = it.getString("title")
val cover = if (it.getString("thumbnail").startsWith("https://")) {
it.getString("thumbnail")
} else {
"https://api.$domain/${j.getString("thumbnail")}"
"https://$cdn${it.getString("thumbnail")}"
}
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
id = id,
url = url,
title = title,
altTitle = it.getString("alternative_names").takeIf { it.isNotBlank() },
publicUrl = url.replace("/comic/", "/series/").toAbsoluteUrl(domain),
description = it.getString("description"),
rating = it.getFloatOrDefault("rating", RATING_UNKNOWN) / 5f,
isNsfw = isNsfwSource,
coverUrl = cover,
tags = setOf(),
state = when (j.getString("status")) {
tags = emptySet(),
state = when (it.getString("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
@ -119,41 +134,28 @@ internal abstract class HeanCms(
source = source,
)
}
}
protected open val datePattern = "yyyy-MM-dd"
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val seriesId = manga.id
val url = "https://$apiPath/chapter/query?page=1&perPage=9999&series_id=$seriesId"
val response = webClient.httpGet(url).parseJson()
val data = response.getJSONArray("data")
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
val slug = manga.url.substringAfterLast('/')
val chapter = root.selectFirstOrThrow("script:containsData(chapter_slug)").data()
.replace("\\", "")
.substringAfter("\"seasons\":")
.substringBefore("}]}],\"children\"")
.split("chapter_name")
.drop(1)
return manga.copy(
altTitle = root.selectFirst("p.text-center.text-gray-400")?.text(),
tags = emptySet(),
author = root.select("div.flex.flex-col.gap-y-2 p:contains(Autor:) strong").text(),
description = root.selectFirst("h5:contains(Desc) + .bg-gray-800")?.html(),
chapters = chapter.mapChapters(reversed = true) { i, it ->
val slugChapter = it.substringAfter("chapter_slug\":\"").substringBefore("\",\"")
val url = "https://$domain/$pathManga/$slug/$slugChapter"
val date = it.substringAfter("created_at\":\"").substringBefore("\",\"").substringBefore("T")
val name = slugChapter.replace("-", " ")
chapters = data.mapJSONIndexed { index, it ->
val chapterUrl =
"/series/${it.getJSONObject("series").getString("series_slug")}/${it.getString("chapter_slug")}"
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1f,
id = it.getLong("id"),
name = it.getString("chapter_name"),
number = (data.length() - index).toFloat(),
volume = 0,
url = url,
url = chapterUrl,
scanlator = null,
uploadDate = dateFormat.tryParse(date),
uploadDate = dateFormat.tryParse(it.getString("created_at").substringBefore("T")),
branch = null,
source = source,
)
@ -177,19 +179,16 @@ internal abstract class HeanCms(
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val tags = doc.selectFirstOrThrow("script:containsData(Genres)").data()
.replace("\\", "")
.substringAfterLast("\"Genres\"")
.split("\",{\"")
.drop(1)
return tags.mapNotNullToSet {
val regex = Regex("\"tags\\\\.*?(\\[.+?])")
val tags = doc.select("script").firstNotNullOf { script ->
regex.find(script.html())?.groupValues?.getOrNull(1)
}.unescapeJson()
return JSONArray(tags).mapJSON {
MangaTag(
key = it.substringAfter("id\":").substringBefore(",\""),
title = it.substringAfter("name\":\"").substringBefore("\"}]").toTitleCase(sourceLocale),
key = it.getInt("id").toString(),
title = it.getString("name").toTitleCase(sourceLocale),
source = source,
)
}
}.toSet()
}
}

@ -1,149 +1,10 @@
package org.koitharu.kotatsu.parsers.site.heancms.en
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.unescapeJson
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("OMEGASCANS", "OmegaScans", "en", ContentType.HENTAI)
internal class OmegaScans(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.OMEGASCANS, "omegascans.org") {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domain)
append("/query?adult=true&query_string=")
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=asc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=desc")
else -> append("latest&order=desc")
}
append("&series_type=All&perPage=$pageSize")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
} else {
"https://api.$domain/${j.getString("thumbnail")}"
}
Manga(
id = j.getLong("id"),
title = j.getString("title"),
altTitle = null,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = j.getFloatOrDefault("rating", RATING_UNKNOWN) / 5f,
isNsfw = true,
coverUrl = cover,
tags = setOf(),
state = when (j.getString("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> null
},
author = j.getStringOrNull("author"),
source = source,
description = j.getString("description"),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val url = buildString {
append("https://api.")
append(domain)
append("/chapter/query?perPage=9999&series_id=" + manga.id)
}
val json = webClient.httpGet(url).parseJson()
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
val chaptersJsonArray = json.getJSONArray("data")
var totalChapters = json.getJSONObject("meta").getInt("total").toFloat()
val chapters = chaptersJsonArray.mapJSON { j ->
val slug = j.getJSONObject("series").getString("series_slug")
val chapterUrl = "https://$domain/$pathManga/$slug/${j.getString("chapter_slug")}"
val date = j.getString("created_at").substringBeforeLast("T")
MangaChapter(
id = j.getLong("id"),
url = chapterUrl,
name = j.getString("chapter_name"),
number = totalChapters--,
volume = 0,
branch = null,
uploadDate = dateFormat.tryParse(date),
scanlator = null,
source = source,
)
}
return manga.copy(
chapters = chapters.reversed(),
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val regex = Regex("\"tags\\\\.*?(\\[.+?])")
val tags = doc.select("script").firstNotNullOf { script ->
regex.find(script.html())?.groupValues?.getOrNull(1)
}.unescapeJson()
return JSONArray(tags).mapJSON {
MangaTag(
key = it.getInt("id").toString(),
title = it.getString("name").toTitleCase(sourceLocale),
source = source,
)
}.toSet()
}
}
HeanCms(context, MangaParserSource.OMEGASCANS, "omegascans.org")

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.parsers.site.heancms.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
internal class ReaperComics(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.REAPERCOMICS, "reaperscans.com") {
override val cdn = "media.reaperscans.com/file/4SRBHm//"
override val paramsUpdated = "updated_at"
}

@ -5,94 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
@Broken // Not dead but changed template and url visualikigai.com
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.YUGEN_MANGAS_ES, "yugenmangas.lat") {
private val domainAlt = "yugenmangas.net"
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domainAlt)
append("/query?query_string=")
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=asc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=desc")
else -> append("latest&order=desc")
}
append("&series_type=Comic&perPage=12")
append("&tags_ids=")
append("[".urlEncoded())
append(filter.tags.joinToString(",") { it.key })
append("]".urlEncoded())
}
null -> {}
}
append("&page=")
append(page.toString())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/series/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
} else {
"https://api.$domainAlt/${j.getString("thumbnail")}"
}
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = cover,
tags = setOf(),
state = when (j.getString("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> null
},
author = null,
source = source,
)
}
}
}
HeanCms(context, MangaParserSource.YUGEN_MANGAS_ES, "yugenmangas.lat")

@ -1,148 +1,11 @@
package org.koitharu.kotatsu.parsers.site.heancms.fr
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("PERF_SCAN", "PerfScan", "fr")
internal class PerfScan(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.PERF_SCAN, "perf-scan.fr") {
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domain)
append("/query?adult=true&query_string=")
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=asc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=desc")
else -> append("latest&order=desc")
}
append("&series_type=All&perPage=")
append(pageSize)
append("&tags_ids=")
append("[".urlEncoded())
filter.tags.joinTo(this, ",") { it.key }
append("]".urlEncoded())
}
null -> {}
}
append("&page=")
append(page)
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
} else {
"https://api.$domain/${j.getString("thumbnail")}"
}
Manga(
id = j.getLong("id"),
title = j.getString("title"),
altTitle = null,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = j.getFloatOrDefault("rating", RATING_UNKNOWN) / 5f,
isNsfw = isNsfwSource,
coverUrl = cover,
tags = setOf(),
state = when (j.getStringOrNull("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> null
},
author = j.getStringOrNull("author"),
source = source,
description = j.getString("description"),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val url = buildString {
append("https://api.")
append(domain)
append("/chapter/query?perPage=9999&series_id=")
append(manga.id)
}
val json = webClient.httpGet(url).parseJson()
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
val chaptersJsonArray = json.getJSONArray("data")
var totalChapters = json.getJSONObject("meta").getInt("total").toFloat()
val chapters = chaptersJsonArray.mapJSON { j ->
val slug = j.getJSONObject("series").getString("series_slug")
val chapterUrl = "https://$domain/$pathManga/$slug/${j.getString("chapter_slug")}"
val date = j.getString("created_at").substringBeforeLast("T")
MangaChapter(
id = j.getLong("id"),
url = chapterUrl,
name = j.getString("chapter_name"),
number = totalChapters--,
volume = 0,
branch = null,
uploadDate = dateFormat.tryParse(date),
scanlator = null,
source = source,
)
}
return manga.copy(
chapters = chapters.reversed(),
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val regex = Regex("\"tags\\\\.*?(\\[.+?])")
val tags = doc.select("script").firstNotNullOf { script ->
regex.find(script.html())?.groupValues?.getOrNull(1)
}.unescapeJson()
return JSONArray(tags).mapJSONToSet {
MangaTag(
key = it.getInt("id").toString(),
title = it.getString("name").toTitleCase(sourceLocale),
source = source,
)
}
}
}
HeanCms(context, MangaParserSource.PERF_SCAN, "perf-scan.fr")

@ -1,150 +1,13 @@
package org.koitharu.kotatsu.parsers.site.heancms.pt
import org.json.JSONArray
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MODESCANLATOR", "ModeScanlator", "pt")
internal class ModeScanlator(
context: MangaLoaderContext,
) : HeanCms(context, MangaParserSource.MODESCANLATOR, "site.modescanlator.net") {
private val domainNoSite = domain.removePrefix("site.")
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://api.")
append(domainNoSite)
append("/query?adult=true&query_string=")
when (filter) {
is MangaListFilter.Search -> {
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
filter.states.oneOrThrowIfMany()?.let {
append("&series_status=")
append(
when (it) {
MangaState.ONGOING -> "Ongoing"
MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus"
else -> ""
},
)
}
append("&orderBy=")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title&order=asc")
SortOrder.ALPHABETICAL_DESC -> append("title&order=desc")
else -> append("latest&order=desc")
}
append("&series_type=All&perPage=")
append(pageSize)
append("&tags_ids=")
append("[".urlEncoded())
filter.tags.joinTo(this, ",") { it.key }
append("]".urlEncoded())
}
null -> {}
}
append("&page=")
append(page)
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/$pathManga/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
} else {
"https://api.$domainNoSite/${j.getString("thumbnail")}"
}
Manga(
id = j.getLong("id"),
title = j.getString("title"),
altTitle = null,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = j.getFloatOrDefault("rating", RATING_UNKNOWN) / 5f,
isNsfw = isNsfwSource,
coverUrl = cover,
tags = setOf(),
state = when (j.getStringOrNull("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
"Hiatus" -> MangaState.PAUSED
else -> null
},
author = j.getStringOrNull("author"),
source = source,
description = j.getString("description"),
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val url = buildString {
append("https://api.")
append(domainNoSite)
append("/chapter/query?perPage=9999&series_id=")
append(manga.id)
}
val json = webClient.httpGet(url).parseJson()
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
val chaptersJsonArray = json.getJSONArray("data")
var totalChapters = json.getJSONObject("meta").getInt("total").toFloat()
val chapters = chaptersJsonArray.mapJSON { j ->
val slug = j.getJSONObject("series").getString("series_slug")
val chapterUrl = "https://$domain/$pathManga/$slug/${j.getString("chapter_slug")}"
val date = j.getString("created_at").substringBeforeLast("T")
MangaChapter(
id = j.getLong("id"),
url = chapterUrl,
name = j.getString("chapter_name"),
number = totalChapters--,
volume = 0,
branch = null,
uploadDate = dateFormat.tryParse(date),
scanlator = null,
source = source,
)
}
return manga.copy(
chapters = chapters.reversed(),
)
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
val regex = Regex("\"tags\\\\.*?(\\[.+?])")
val tags = doc.select("script").firstNotNullOf { script ->
regex.find(script.html())?.groupValues?.getOrNull(1)
}.unescapeJson()
return JSONArray(tags).mapJSONToSet {
MangaTag(
key = it.getInt("id").toString(),
title = it.getString("name").toTitleCase(sourceLocale),
source = source,
)
}
}
internal class ModeScanlator(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.MODESCANLATOR, "site.modescanlator.net") {
override val apiPath = "api.modescanlator.net"
}

Loading…
Cancel
Save