MediocreToons: Add source (#2077)
parent
7c264b6af7
commit
a93140fc36
@ -0,0 +1,274 @@
|
||||
package org.koitharu.kotatsu.parsers.site.pt
|
||||
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.parseSafe
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
|
||||
@MangaSourceParser("MEDIOCRETOONS", "MediocreToons", "pt")
|
||||
internal class MediocreToons(context: MangaLoaderContext) : PagedMangaParser(
|
||||
context,
|
||||
source = MangaParserSource.MEDIOCRETOONS,
|
||||
pageSize = 20,
|
||||
) {
|
||||
override val configKeyDomain = ConfigKey.Domain("mediocretoons.com")
|
||||
private val apiUrl = "https://api.mediocretoons.com"
|
||||
private val cdnUrl = "https://storage.mediocretoons.com"
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
isMultipleTagsSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||
return MangaListFilterOptions(
|
||||
availableTags = fetchAvailableTags(),
|
||||
availableStates = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
MangaState.ABANDONED,
|
||||
),
|
||||
availableContentTypes = EnumSet.of(
|
||||
ContentType.MANGA,
|
||||
ContentType.HENTAI,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val apiHeaders: Headers
|
||||
get() = Headers.Builder().add("Referer", "https://$domain/").add("Origin", "https://$domain").build()
|
||||
|
||||
private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", sourceLocale)
|
||||
|
||||
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
val url = when {
|
||||
!filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.types.isNotEmpty() -> buildSearchUrl(
|
||||
page,
|
||||
filter,
|
||||
)
|
||||
|
||||
else -> {
|
||||
"$apiUrl/obras".toHttpUrl().newBuilder().addQueryParameter("limite", pageSize.toString())
|
||||
.addQueryParameter("pagina", page.toString()).addQueryParameter("temCapitulo", "true").build()
|
||||
}
|
||||
}
|
||||
|
||||
val response = webClient.httpGet(url, apiHeaders).parseJson()
|
||||
val results = response.optJSONArray("data") ?: return emptyList()
|
||||
return results.mapJSON { parseMangaFromJson(it) }
|
||||
}
|
||||
|
||||
private fun buildSearchUrl(page: Int, filter: MangaListFilter): HttpUrl {
|
||||
val builder = "$apiUrl/obras".toHttpUrl().newBuilder().addQueryParameter("limite", pageSize.toString())
|
||||
.addQueryParameter("pagina", page.toString()).addQueryParameter("temCapitulo", "true")
|
||||
|
||||
// Add search query
|
||||
if (!filter.query.isNullOrEmpty()) {
|
||||
builder.addQueryParameter("string", filter.query)
|
||||
}
|
||||
|
||||
// Add tags
|
||||
filter.tags.forEach { tag ->
|
||||
builder.addQueryParameter("tags[]", tag.key)
|
||||
}
|
||||
|
||||
if (filter.types.isNotEmpty()) {
|
||||
filter.types.forEach { contentType ->
|
||||
val formatId = when (contentType) {
|
||||
ContentType.MANGA -> "5"
|
||||
ContentType.HENTAI -> "8"
|
||||
else -> null
|
||||
}
|
||||
formatId?.let { builder.addQueryParameter("formato[]", it) }
|
||||
}
|
||||
}
|
||||
|
||||
// Add status
|
||||
filter.states.oneOrThrowIfMany()?.let { state ->
|
||||
val statusId = when (state) {
|
||||
MangaState.ONGOING -> "2"
|
||||
MangaState.FINISHED -> "4"
|
||||
MangaState.PAUSED -> "3"
|
||||
else -> null
|
||||
}
|
||||
statusId?.let { builder.addQueryParameter("status", it) }
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun parseMangaFromJson(json: JSONObject): Manga {
|
||||
val id = json.getInt("id")
|
||||
val name = json.getString("nome")
|
||||
val slug = name.lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-')
|
||||
val coverPath = json.optString("imagem", "")
|
||||
|
||||
val coverUrl = when {
|
||||
coverPath.startsWith("http") -> coverPath
|
||||
coverPath.isNotEmpty() -> "$cdnUrl/obras/$id/$coverPath"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val rating = RATING_UNKNOWN
|
||||
|
||||
return Manga(
|
||||
id = generateUid(id.toLong()),
|
||||
title = name,
|
||||
url = "/obra/$id/$slug",
|
||||
publicUrl = "https://$domain/obra/$id/$slug",
|
||||
coverUrl = coverUrl,
|
||||
source = source,
|
||||
rating = rating,
|
||||
altTitles = emptySet(),
|
||||
contentRating = null,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
authors = emptySet(),
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
chapters = null,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val mangaId = manga.url.substringAfter("/obra/").substringBefore("/")
|
||||
val response = webClient.httpGet("$apiUrl/obras/$mangaId", apiHeaders).parseJson()
|
||||
|
||||
val description = response.optString("descricao").replace(Regex("</?[^>]+>"), "").replace("\\/", "/")
|
||||
.replace(Regex("\\s+"), " ").trim()
|
||||
|
||||
val status = response.optJSONObject("status")?.let { statusObj ->
|
||||
when (statusObj.optString("nome").lowercase()) {
|
||||
"ativo", "em andamento" -> MangaState.ONGOING
|
||||
"concluído", "completo" -> MangaState.FINISHED
|
||||
"hiato" -> MangaState.PAUSED
|
||||
"cancelado" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val tags = response.optJSONArray("tags")?.mapJSON { tagJson ->
|
||||
MangaTag(
|
||||
key = tagJson.getInt("id").toString(),
|
||||
title = tagJson.getString("nome").toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
}?.toSet() ?: emptySet()
|
||||
|
||||
val chapters = response.optJSONArray("capitulos")?.mapJSON { chapterJson ->
|
||||
parseChapter(chapterJson)
|
||||
} ?: emptyList()
|
||||
|
||||
return manga.copy(
|
||||
title = response.optString("nome", manga.title),
|
||||
description = description,
|
||||
state = status,
|
||||
tags = tags,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseChapter(json: JSONObject): MangaChapter {
|
||||
val chapterId = json.getInt("id")
|
||||
val chapterName = json.getString("nome")
|
||||
val chapterDate = json.optString("criado_em")
|
||||
|
||||
val chapterNumber = json.optString("numero", "0").toFloatOrNull() ?: 0f
|
||||
|
||||
return MangaChapter(
|
||||
id = generateUid(chapterId.toLong()),
|
||||
title = "Capítulo $chapterName",
|
||||
number = chapterNumber,
|
||||
url = "/capitulo/$chapterId",
|
||||
uploadDate = chapterDateFormat.parseSafe(chapterDate),
|
||||
source = source,
|
||||
volume = 0,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val chapterId = chapter.url.substringAfter("/capitulo/")
|
||||
|
||||
val response = webClient.httpGet("$apiUrl/capitulos/$chapterId", apiHeaders).parseJson()
|
||||
|
||||
val pagesArray = response.optJSONArray("paginas") ?: throw Exception("No pages found in chapter")
|
||||
|
||||
val obraInfo = response.optJSONObject("obra") ?: throw Exception("Obra information not found")
|
||||
val obraId = obraInfo.getInt("id")
|
||||
val chapterNumber = response.optString("numero", "")
|
||||
|
||||
return pagesArray.mapJSONNotNull { pageJson ->
|
||||
val pageSrc = pageJson.optString("src")
|
||||
|
||||
if (pageSrc.isEmpty()) return@mapJSONNotNull null
|
||||
|
||||
val imageUrl = when {
|
||||
pageSrc.startsWith("http") -> pageSrc
|
||||
else -> "$cdnUrl/obras/$obraId/capitulos/$chapterNumber/$pageSrc"
|
||||
}
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(imageUrl),
|
||||
url = imageUrl,
|
||||
source = source,
|
||||
preview = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAvailableTags(): Set<MangaTag> {
|
||||
val url = "$apiUrl/tags"
|
||||
val body = webClient.httpGet(url, apiHeaders).body?.string()?.trim()
|
||||
|
||||
if (body == null) return emptySet()
|
||||
|
||||
val tagsArray = if (body.startsWith("[")) {
|
||||
org.json.JSONArray(body)
|
||||
} else {
|
||||
val json = JSONObject(body)
|
||||
json.optJSONArray("tags") ?: json.optJSONArray("data") ?: org.json.JSONArray()
|
||||
}
|
||||
|
||||
return tagsArray.mapJSONNotNull { tagJson ->
|
||||
MangaTag(
|
||||
key = tagJson.getInt("id").toString(),
|
||||
title = tagJson.getString("nome").toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue