ArabsHentai: Add source (#2063)
parent
0477fe0659
commit
f61f5329e3
@ -1 +1 @@
|
||||
total: 1244
|
||||
total: 1245
|
||||
@ -0,0 +1,382 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.ar
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
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.exception.ParseException
|
||||
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.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
|
||||
import org.koitharu.kotatsu.parsers.util.src
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
|
||||
@MangaSourceParser("ARABSHENTAI", "Arabs Hentai", "ar", ContentType.HENTAI)
|
||||
internal class ArabsHentai(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaParserSource.ARABSHENTAI, domain = "arabshentai.com", 25) {
|
||||
|
||||
override val withoutAjax = true
|
||||
override val sourceLocale: Locale = Locale("ar")
|
||||
override val listUrl = "manga/"
|
||||
override val datePattern = "yyyy-MM-dd"
|
||||
override val selectDate = ".chapterdate"
|
||||
override val selectDesc = "#manga-info .wp-content p"
|
||||
override val selectState = "#manga-info div b:contains(حالة المانجا)"
|
||||
override val selectAlt = "#manga-info div b:contains(أسماء أُخرى) + span"
|
||||
override val selectGenre = ".data .sgeneros a"
|
||||
override val selectPage = ".chapter_image img.wp-manga-chapter-img"
|
||||
override val selectChapter = "#chapter-list a[href*='/manga/'], .oneshot-reader"
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
isAuthorSearchSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
||||
availableTags = fetchAvailableTags(),
|
||||
availableStates = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
MangaState.ABANDONED,
|
||||
MangaState.PAUSED,
|
||||
),
|
||||
availableContentTypes = EnumSet.of(
|
||||
ContentType.MANGA,
|
||||
ContentType.MANHWA,
|
||||
ContentType.MANHUA,
|
||||
ContentType.DOUJINSHI,
|
||||
ContentType.ONE_SHOT,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
val pages = page + 1
|
||||
|
||||
val url = when {
|
||||
!filter.query.isNullOrEmpty() || filter.tags.isNotEmpty() -> {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages)
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query?.urlEncoded() ?: "")
|
||||
|
||||
filter.tags.forEach { tag ->
|
||||
append("&genre%5B%5D=")
|
||||
append(tag.key.urlEncoded())
|
||||
}
|
||||
|
||||
if (filter.tags.size > 1) {
|
||||
append("&op=1")
|
||||
}
|
||||
|
||||
filter.states.forEach { state ->
|
||||
append("&status%5B%5D=")
|
||||
append(
|
||||
when (state) {
|
||||
MangaState.ONGOING -> "on-going"
|
||||
MangaState.FINISHED -> "end"
|
||||
MangaState.ABANDONED -> "canceled"
|
||||
MangaState.PAUSED -> "on-hold"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
append("&alternative=&author=&artist=")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/manga/")
|
||||
if (pages > 1) {
|
||||
append("page/")
|
||||
append(pages)
|
||||
append("/")
|
||||
}
|
||||
|
||||
val params = mutableListOf<String>()
|
||||
|
||||
filter.types.forEach { type ->
|
||||
params.add(
|
||||
"type=" + when (type) {
|
||||
ContentType.MANGA -> "manga"
|
||||
ContentType.MANHWA -> "manhwa"
|
||||
ContentType.MANHUA -> "manhua"
|
||||
ContentType.DOUJINSHI -> "doujinshi"
|
||||
ContentType.ONE_SHOT -> "one-shot"
|
||||
else -> "manga"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
params.add(
|
||||
"orderby=" + when (order) {
|
||||
SortOrder.NEWEST -> "new-manga"
|
||||
SortOrder.ALPHABETICAL -> "alphabet"
|
||||
SortOrder.UPDATED -> "new_chapter"
|
||||
else -> "new_chapter"
|
||||
},
|
||||
)
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let { state ->
|
||||
params.add(
|
||||
"state=" + when (state) {
|
||||
MangaState.ONGOING -> "on-going"
|
||||
MangaState.FINISHED -> "end"
|
||||
MangaState.ABANDONED -> "canceled"
|
||||
MangaState.PAUSED -> "on-hold"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
append(params.joinToString("&"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override fun parseMangaList(doc: Document): List<Manga> {
|
||||
val searchElements = doc.select(".search-page .result-item article:not(:has(.tvshows))")
|
||||
|
||||
if (searchElements.isNotEmpty()) {
|
||||
return searchElements.map { element ->
|
||||
val titleElement = element.selectFirstOrThrow(".details .title a")
|
||||
val href = titleElement.attrAsRelativeUrl("href")
|
||||
|
||||
val coverUrl = element.run {
|
||||
val postId = attr("id").substringAfter("post-").ifBlank { null }
|
||||
val img = selectFirst(".image .thumbnail a img")
|
||||
val lazySrc = img?.attr("data-src")
|
||||
|
||||
if (postId != null && !lazySrc.isNullOrBlank() && lazySrc.contains("/uploads/")) {
|
||||
"${lazySrc.substringBeforeLast('/')}/cover-$postId.webp"
|
||||
} else {
|
||||
img?.src()
|
||||
}
|
||||
}
|
||||
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
title = titleElement.text(),
|
||||
coverUrl = coverUrl,
|
||||
source = source,
|
||||
contentRating = ContentRating.ADULT,
|
||||
altTitles = emptySet(),
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
authors = emptySet(),
|
||||
state = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return doc.select("#archive-content .wp-manga").map { element ->
|
||||
val titleElement = element.selectFirstOrThrow(".data h3 a")
|
||||
val href = titleElement.attrAsRelativeUrl("href")
|
||||
|
||||
val coverUrl = element.run {
|
||||
val postId = attr("id").substringAfter("post-").ifBlank { null }
|
||||
val img = selectFirst("a .poster img")
|
||||
val lazySrc = img?.attr("data-src")
|
||||
|
||||
if (postId != null && !lazySrc.isNullOrBlank() && lazySrc.contains("/uploads/")) {
|
||||
"${lazySrc.substringBeforeLast('/')}/cover-$postId.webp"
|
||||
} else {
|
||||
img?.src()
|
||||
}
|
||||
}
|
||||
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
title = titleElement.text(),
|
||||
coverUrl = coverUrl,
|
||||
source = source,
|
||||
contentRating = ContentRating.ADULT,
|
||||
altTitles = emptySet(), rating = RATING_UNKNOWN, tags = emptySet(), authors = emptySet(), state = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/تصنيفات/").parseHtml()
|
||||
|
||||
return doc.select("#archive-content ul.genre-list li.item-genre .genre-data a")
|
||||
.mapNotNullToSet { a ->
|
||||
val key = a.attr("href").substringAfter(tagPrefix).removeSuffix("/")
|
||||
val title = a.ownText().trim()
|
||||
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = title,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
|
||||
val oneshotReader = doc.selectFirst(".oneshot-reader")
|
||||
if (oneshotReader != null) {
|
||||
val firstImageLink = oneshotReader.selectFirst(".image-item a[href*='?style=paged']")
|
||||
val chapterUrl = firstImageLink?.attr("href")?.substringBeforeLast("?") ?: manga.url
|
||||
|
||||
return listOf(
|
||||
MangaChapter(
|
||||
id = generateUid(chapterUrl),
|
||||
title = "ونشوت",
|
||||
number = 1f,
|
||||
volume = 0,
|
||||
url = chapterUrl.toRelativeUrl(domain),
|
||||
uploadDate = 0L,
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return doc.select("#chapter-list a[href*='/manga/']").mapChapters(reversed = true) { i, element ->
|
||||
val href = element.attr("href")
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
title = element.select(".chapternum").text().ifEmpty { "Chapter ${i + 1}" },
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = href.toRelativeUrl(domain),
|
||||
uploadDate = parseChapterDate(dateFormat, element.select(selectDate).text()),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
|
||||
val chaptersDeferred = async { getChapters(manga, doc) }
|
||||
|
||||
manga.copy(
|
||||
title = doc.selectFirst(".sheader .data h1")?.text() ?: manga.title,
|
||||
coverUrl = doc.selectFirst(".sheader .poster img")?.src() ?: manga.coverUrl,
|
||||
description = doc.select(selectDesc).text(),
|
||||
altTitles = doc.select(selectAlt)
|
||||
.text()
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.toSet(),
|
||||
authors = doc.select("#manga-info div b:contains(الكاتب) + span a")
|
||||
.mapNotNullToSet { it.text().takeIf { text -> text.isNotEmpty() } },
|
||||
tags = doc.select(selectGenre)
|
||||
.mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").substringAfter(tagPrefix).removeSuffix("/"),
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
rating = doc.selectFirst(".dt_rating_vgs")?.text()?.toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN,
|
||||
state = parseStatus(doc.select("#manga-info div b:contains(حالة المانجا) + span").text()),
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String): MangaState? {
|
||||
return when {
|
||||
status.contains("مستمر", ignoreCase = true) -> MangaState.ONGOING
|
||||
status.contains("مكتمل", ignoreCase = true) -> MangaState.FINISHED
|
||||
status.contains("متوقف", ignoreCase = true) -> MangaState.PAUSED
|
||||
status.contains("ملغية", ignoreCase = true) -> MangaState.ABANDONED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val oneshotReader = doc.selectFirst(".oneshot-reader")
|
||||
return oneshotReader?.select(".image-item img.oneshot-chapter-img")?.map { img ->
|
||||
val url = img.imgAttr() ?: throw ParseException("Image URL not found", fullUrl)
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
?: doc.select(selectPage).map { img ->
|
||||
val url = img.imgAttr() ?: throw ParseException("Image URL not found", fullUrl)
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.imgAttr(): String? {
|
||||
return when {
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
hasAttr("src") && attr("src").isNotEmpty() -> attr("abs:src")
|
||||
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
|
||||
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("bv-data-src") -> attr("bv-data-src")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue