InManga: Add source (#2073)
parent
8964bb1584
commit
134656b835
@ -1 +1 @@
|
|||||||
total: 1245
|
total: 1246
|
||||||
@ -0,0 +1,248 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.es
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
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.RATING_UNKNOWN
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.toJSONObjectOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
@MangaSourceParser("INMANGA", "InManga", "es", ContentType.MANGA)
|
||||||
|
internal class InMangaParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||||
|
context,
|
||||||
|
source = MangaParserSource.INMANGA,
|
||||||
|
pageSize = 10,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("inmanga.com")
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.ALPHABETICAL,
|
||||||
|
SortOrder.RELEVANCE,
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
SortOrder.NEWEST,
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = MangaListFilterCapabilities(
|
||||||
|
isSearchSupported = true,
|
||||||
|
isSearchWithFiltersSupported = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||||
|
return MangaListFilterOptions(
|
||||||
|
availableTags = emptySet(),
|
||||||
|
availableContentTypes = EnumSet.of(ContentType.MANGA),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val postHeaders = Headers.Builder()
|
||||||
|
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||||
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val imageCDN = "https://pack-yak.intomanga.com/"
|
||||||
|
private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
|
||||||
|
|
||||||
|
private fun buildFormData(
|
||||||
|
page: Int,
|
||||||
|
order: SortOrder,
|
||||||
|
query: String = "",
|
||||||
|
genres: Set<String> = emptySet(),
|
||||||
|
): Map<String, String> {
|
||||||
|
val sortValue = when (order) {
|
||||||
|
SortOrder.ALPHABETICAL -> "5" // Nombre
|
||||||
|
SortOrder.RELEVANCE -> "2" // Relevancia
|
||||||
|
SortOrder.POPULARITY -> "1" // Vistos
|
||||||
|
SortOrder.NEWEST -> "4" // Recién agregado
|
||||||
|
SortOrder.UPDATED -> "3" // Recién actualizado
|
||||||
|
else -> "3" // Default to Recién actualizado
|
||||||
|
}
|
||||||
|
|
||||||
|
val formData = mutableMapOf(
|
||||||
|
"filter[queryString]" to query,
|
||||||
|
"filter[skip]" to "${(page - 1) * 10}",
|
||||||
|
"filter[take]" to "10",
|
||||||
|
"filter[sortby]" to sortValue,
|
||||||
|
"filter[broadcastStatus]" to "0",
|
||||||
|
"filter[onlyFavorites]" to "false",
|
||||||
|
"d" to "",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add genres
|
||||||
|
if (genres.isEmpty()) {
|
||||||
|
formData["filter[generes][]"] = "-1"
|
||||||
|
} else {
|
||||||
|
genres.forEachIndexed { index, genre ->
|
||||||
|
formData["filter[generes][$index]"] = genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
val genres = filter.tags.map { it.key }.toSet()
|
||||||
|
val formData = buildFormData(page, order, filter.query ?: "", genres)
|
||||||
|
|
||||||
|
val response = webClient.httpPost(
|
||||||
|
"https://$domain/manga/getMangasConsultResult".toHttpUrl(),
|
||||||
|
formData,
|
||||||
|
postHeaders,
|
||||||
|
)
|
||||||
|
|
||||||
|
val document = response.parseHtml()
|
||||||
|
return document.select("body > a").map { element ->
|
||||||
|
parseMangaFromElement(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMangaFromElement(element: Element): Manga {
|
||||||
|
val url = element.attr("href")
|
||||||
|
val title = element.select("h4.m0").text()
|
||||||
|
val coverUrl = element.select("img").attr("abs:data-src")
|
||||||
|
|
||||||
|
return Manga(
|
||||||
|
id = url.hashCode().toLong(),
|
||||||
|
title = title,
|
||||||
|
altTitles = emptySet(),
|
||||||
|
url = url,
|
||||||
|
publicUrl = "https://$domain$url",
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
contentRating = null,
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
authors = emptySet(),
|
||||||
|
description = null,
|
||||||
|
chapters = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val response = webClient.httpGet(manga.publicUrl.toHttpUrl())
|
||||||
|
val document = response.parseHtml()
|
||||||
|
|
||||||
|
val infoPanel = document.select("div.col-md-3 div.panel.widget")
|
||||||
|
val contentPanel = document.select("div.col-md-9")
|
||||||
|
|
||||||
|
val coverUrl = infoPanel.select("img").attr("abs:src")
|
||||||
|
val statusText = infoPanel.select("a.list-group-item:contains(estado) span").text()
|
||||||
|
val title = contentPanel.select("h1").text()
|
||||||
|
val description = contentPanel.select("div.panel-body").text()
|
||||||
|
|
||||||
|
val chapters = getChapters(manga)
|
||||||
|
|
||||||
|
return manga.copy(
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
state = parseStatus(statusText),
|
||||||
|
chapters = chapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String?): MangaState? {
|
||||||
|
return when {
|
||||||
|
status == null -> null
|
||||||
|
status.contains("En emisión") -> MangaState.ONGOING
|
||||||
|
status.contains("Finalizado") -> MangaState.FINISHED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getChapters(manga: Manga): List<MangaChapter> {
|
||||||
|
val mangaId = manga.url.substringAfterLast("/")
|
||||||
|
val response = webClient.httpGet(
|
||||||
|
"https://$domain/chapter/getall?mangaIdentification=$mangaId".toHttpUrl(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// The server returns a JSON with data property that contains a string with the JSON,
|
||||||
|
// so is necessary to decode twice.
|
||||||
|
val json = response.parseJson()
|
||||||
|
val dataString = json.getStringOrNull("data") ?: return emptyList()
|
||||||
|
|
||||||
|
val dataJson = dataString.toJSONObjectOrNull() ?: return emptyList()
|
||||||
|
if (!dataJson.getBooleanOrDefault("success", false)) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val chaptersArray = dataJson.optJSONArray("result") ?: return emptyList()
|
||||||
|
|
||||||
|
return chaptersArray.mapJSON { chapterJson ->
|
||||||
|
parseChapterFromJson(chapterJson)
|
||||||
|
}.sortedBy { it.number }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterFromJson(chapterJson: JSONObject): MangaChapter {
|
||||||
|
val identification = chapterJson.getStringOrNull("Identification") ?: ""
|
||||||
|
val friendlyChapterNumber = chapterJson.getStringOrNull("FriendlyChapterNumber") ?: ""
|
||||||
|
val number = chapterJson.optDouble("Number").toFloat()
|
||||||
|
val registrationDate = chapterJson.getStringOrNull("RegistrationDate") ?: ""
|
||||||
|
|
||||||
|
return MangaChapter(
|
||||||
|
id = identification.hashCode().toLong(),
|
||||||
|
title = "Chapter $friendlyChapterNumber",
|
||||||
|
number = number,
|
||||||
|
volume = 0,
|
||||||
|
url = "/chapter/chapterIndexControls?identification=$identification",
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = parseChapterDate(registrationDate),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(dateString: String): Long {
|
||||||
|
return try {
|
||||||
|
chapterDateFormat.parse(dateString)?.time ?: 0L
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val response = webClient.httpGet(
|
||||||
|
"https://$domain${chapter.url}".toHttpUrl(),
|
||||||
|
)
|
||||||
|
val document = response.parseHtml()
|
||||||
|
|
||||||
|
val ch = document.select("[id=\"FriendlyChapterNumberUrl\"]").attr("value")
|
||||||
|
val title = document.select("[id=\"FriendlyMangaName\"]").attr("value")
|
||||||
|
|
||||||
|
return document.select("img.ImageContainer").mapIndexed { index, img ->
|
||||||
|
val imageId = img.attr("id")
|
||||||
|
val imageUrl = "$imageCDN/images/manga/$title/chapter/$ch/page/${index + 1}/$imageId"
|
||||||
|
|
||||||
|
MangaPage(
|
||||||
|
id = index.toLong(),
|
||||||
|
url = imageUrl,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue