Hentaman: Add source (#2078)
parent
fd0df2414e
commit
31999d97a0
@ -1 +1 @@
|
|||||||
total: 1247
|
total: 1248
|
||||||
@ -0,0 +1,308 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.ar
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
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.ContentRating
|
||||||
|
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.mapJSONIndexed
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.toJSONArrayOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseSafe
|
||||||
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
@MangaSourceParser("HENTAMAN", "Hentaman", "ar", ContentType.HENTAI)
|
||||||
|
internal class HentaMan(context: MangaLoaderContext) : PagedMangaParser(
|
||||||
|
context,
|
||||||
|
source = MangaParserSource.HENTAMAN,
|
||||||
|
pageSize = 12,
|
||||||
|
) {
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("hentaman.net")
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
SortOrder.POPULARITY_TODAY,
|
||||||
|
SortOrder.POPULARITY_WEEK,
|
||||||
|
SortOrder.RATING,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val filterCapabilities: MangaListFilterCapabilities
|
||||||
|
get() = MangaListFilterCapabilities(
|
||||||
|
isSearchSupported = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", sourceLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||||
|
val tags = fetchAvailableTags()
|
||||||
|
return MangaListFilterOptions(
|
||||||
|
availableTags = tags,
|
||||||
|
availableStates = EnumSet.of(
|
||||||
|
MangaState.ONGOING,
|
||||||
|
MangaState.FINISHED,
|
||||||
|
MangaState.PAUSED,
|
||||||
|
MangaState.ABANDONED,
|
||||||
|
),
|
||||||
|
availableContentTypes = EnumSet.of(
|
||||||
|
ContentType.MANGA,
|
||||||
|
ContentType.MANHWA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val baseUrl get() = "https://$domain"
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||||
|
|
||||||
|
val url = when {
|
||||||
|
!filter.query.isNullOrEmpty() -> {
|
||||||
|
val searchUrl = "$baseUrl/manga/search".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("query", filter.query)
|
||||||
|
.build()
|
||||||
|
val response = webClient.httpGet(searchUrl).parseJson()
|
||||||
|
val results = response.optJSONArray("results") ?: return emptyList()
|
||||||
|
return results.mapJSON { parseMangaFromJson(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.tags.isNotEmpty() -> {
|
||||||
|
val tag = filter.tags.first()
|
||||||
|
"$baseUrl/list/genre/${tag.key}".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.states.isNotEmpty() -> {
|
||||||
|
val state = when (filter.states.first()) {
|
||||||
|
MangaState.ONGOING -> "مستمر"
|
||||||
|
MangaState.FINISHED -> "مكتمل"
|
||||||
|
MangaState.PAUSED -> "متوقف"
|
||||||
|
MangaState.ABANDONED -> "ملغى"
|
||||||
|
else -> "مستمر"
|
||||||
|
}
|
||||||
|
"$baseUrl/list/status/$state".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.types.isNotEmpty() -> {
|
||||||
|
val type = when (filter.types.first()) {
|
||||||
|
ContentType.MANGA -> "مانجا"
|
||||||
|
ContentType.MANHWA -> "مانهوا"
|
||||||
|
else -> "مانجا"
|
||||||
|
}
|
||||||
|
"$baseUrl/list/type/$type".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
when (order) {
|
||||||
|
SortOrder.POPULARITY -> "$baseUrl/list/top/total_views"
|
||||||
|
SortOrder.POPULARITY_TODAY -> "$baseUrl/list/top/today"
|
||||||
|
SortOrder.POPULARITY_WEEK -> "$baseUrl/list/top/week"
|
||||||
|
SortOrder.RATING -> "$baseUrl/list/top/all-time"
|
||||||
|
else -> "$baseUrl/list/status/completed"
|
||||||
|
}.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val doc = webClient.httpGet(url).parseHtml()
|
||||||
|
return parseListFromDocument(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseListFromDocument(doc: Document): List<Manga> {
|
||||||
|
val dataPage = doc.selectFirst("div#app")?.attr("data-page") ?: return emptyList()
|
||||||
|
val pageData = JSONObject(dataPage)
|
||||||
|
val props = pageData.optJSONObject("props") ?: return emptyList()
|
||||||
|
|
||||||
|
val mangasData = when {
|
||||||
|
props.has("data") -> {
|
||||||
|
val data = props.getJSONObject("data")
|
||||||
|
data.optJSONObject("mangas") ?: data
|
||||||
|
}
|
||||||
|
|
||||||
|
props.has("mangas") -> props.getJSONObject("mangas")
|
||||||
|
else -> return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangasArray = mangasData.optJSONArray("data") ?: return emptyList()
|
||||||
|
return mangasArray.mapJSON { parseMangaFromJson(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMangaFromJson(json: JSONObject): Manga {
|
||||||
|
val title = json.getString("title")
|
||||||
|
val slug = json.getString("slug").urlEncoded()
|
||||||
|
val cover = json.getString("cover")
|
||||||
|
val url = "/manga/$slug"
|
||||||
|
|
||||||
|
val rating = json.optString("average_rating").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN
|
||||||
|
|
||||||
|
return Manga(
|
||||||
|
id = generateUid(url),
|
||||||
|
title = title,
|
||||||
|
altTitles = emptySet(),
|
||||||
|
url = url,
|
||||||
|
publicUrl = "$baseUrl$url",
|
||||||
|
rating = rating,
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
coverUrl = "$baseUrl/storage/covers/md/$cover",
|
||||||
|
largeCoverUrl = "$baseUrl/storage/covers/lg/$cover",
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
authors = emptySet(),
|
||||||
|
description = null,
|
||||||
|
chapters = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val doc = webClient.httpGet(manga.publicUrl.toHttpUrl()).parseHtml()
|
||||||
|
val dataPage = doc.selectFirst("div#app")?.attr("data-page") ?: return manga
|
||||||
|
|
||||||
|
val pageData = JSONObject(dataPage)
|
||||||
|
val props = pageData.getJSONObject("props")
|
||||||
|
val mangaData = props.getJSONObject("manga")
|
||||||
|
|
||||||
|
val description = mangaData.optString("description")
|
||||||
|
val author = mangaData.optString("author")
|
||||||
|
val status = mangaData.optString("status")
|
||||||
|
val rating = mangaData.optString("average_rating").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN
|
||||||
|
|
||||||
|
val state = when (status) {
|
||||||
|
"ongoing", "مستمر" -> MangaState.ONGOING
|
||||||
|
"completed", "مكتمل" -> MangaState.FINISHED
|
||||||
|
"paused", "متوقف" -> MangaState.PAUSED
|
||||||
|
"cancelled", "ملغى" -> MangaState.ABANDONED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val genres = mangaData.optJSONArray("genres")?.mapJSON { genreJson ->
|
||||||
|
MangaTag(
|
||||||
|
key = genreJson.getString("slug"),
|
||||||
|
title = genreJson.getString("title"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}?.toSet() ?: emptySet()
|
||||||
|
|
||||||
|
val chapters =
|
||||||
|
props.getJSONObject("chapters").optJSONArray("data")?.mapJSONNotNull { chapterJson ->
|
||||||
|
parseChapter(chapterJson, manga.url)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
return manga.copy(
|
||||||
|
title = mangaData.getString("title"),
|
||||||
|
description = description,
|
||||||
|
state = state,
|
||||||
|
tags = genres,
|
||||||
|
chapters = chapters,
|
||||||
|
authors = setOfNotNull(author.takeIf { !it.isNullOrEmpty() && it != "null" }),
|
||||||
|
rating = rating,
|
||||||
|
contentRating = ContentRating.ADULT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapter(json: JSONObject, mangaUrl: String): MangaChapter? {
|
||||||
|
// Skip locked chapters
|
||||||
|
val shortLink = json.optString("short_link")
|
||||||
|
if (!shortLink.isNullOrEmpty() && shortLink != "null") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = json.getLong("id")
|
||||||
|
val chapterNumberStr = json.getString("chapter_number")
|
||||||
|
val chapterNumber = chapterNumberStr.toFloatOrNull() ?: 0f
|
||||||
|
val chapterName = json.optString("chapter_name").takeIf { it.isNotBlank() && it != "null" }
|
||||||
|
val createdAt = json.getString("created_at")
|
||||||
|
|
||||||
|
val title = chapterName ?: "الفصل $chapterNumberStr"
|
||||||
|
|
||||||
|
return MangaChapter(
|
||||||
|
id = generateUid(id),
|
||||||
|
title = title,
|
||||||
|
number = chapterNumber,
|
||||||
|
volume = 0,
|
||||||
|
url = "$mangaUrl/$chapterNumberStr",
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = dateFormat.parseSafe(createdAt),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val chapterUrl = "$baseUrl${chapter.url}"
|
||||||
|
val doc = webClient.httpGet(chapterUrl).parseHtml()
|
||||||
|
val dataPage = doc.selectFirst("div#app")?.attr("data-page") ?: return emptyList()
|
||||||
|
|
||||||
|
val pageData = JSONObject(dataPage)
|
||||||
|
val component = pageData.getString("component")
|
||||||
|
// Skip locked chapters
|
||||||
|
if (component == "LockedChapter") {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val props = pageData.getJSONObject("props")
|
||||||
|
val data = props.getJSONObject("data")
|
||||||
|
val manga = data.getJSONObject("manga")
|
||||||
|
val currentChapter = data.getJSONObject("current_chapter")
|
||||||
|
|
||||||
|
val imagesJsonString = currentChapter.getString("images")
|
||||||
|
val chapterDir = currentChapter.getString("dir")
|
||||||
|
val mangaDirectory = manga.getString("directory")
|
||||||
|
|
||||||
|
val imagesArray = imagesJsonString.toJSONArrayOrNull() ?: return emptyList()
|
||||||
|
|
||||||
|
return imagesArray.mapJSONIndexed { index, imageJson ->
|
||||||
|
val imageName = imageJson.getString("name")
|
||||||
|
val imageUrl = "$baseUrl/storage/mangas/low/$mangaDirectory/$chapterDir/$imageName"
|
||||||
|
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid("${chapter.id}_$index"),
|
||||||
|
url = imageUrl,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchAvailableTags(): Set<MangaTag> {
|
||||||
|
val doc = webClient.httpGet("$baseUrl/genres".toHttpUrl()).parseHtml()
|
||||||
|
val dataPage = doc.selectFirst("div#app")?.attr("data-page") ?: return emptySet()
|
||||||
|
val pageData = JSONObject(dataPage)
|
||||||
|
val genres = pageData.getJSONObject("props").optJSONArray("genres") ?: return emptySet()
|
||||||
|
|
||||||
|
return genres.mapJSON { genreJson ->
|
||||||
|
MangaTag(
|
||||||
|
key = genreJson.getString("slug"),
|
||||||
|
title = genreJson.getString("title"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue