RocksManga: Update site structure (#2059)

master
Naga 9 months ago committed by GitHub
parent 02ac1cb896
commit 5483453f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,35 +1,264 @@
package org.koitharu.kotatsu.parsers.site.madara.ar
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
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.attrAsRelativeUrlOrNull
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.parseFailed
import java.text.SimpleDateFormat
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.requireSrc
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.util.Calendar
import java.util.EnumSet
@MangaSourceParser("ROCKSMANGA", "RocksManga", "ar")
internal class RocksManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.ROCKSMANGA, "rocksmanga.com") {
override val selectChapter = "ul#chapter-list li.chapter-item"
MadaraParser(context, MangaParserSource.ROCKSMANGA, "rockscans.org") {
override val withoutAjax = false
override val datePattern = "d MMMM yyyy"
override val selectDate = ".ch-post-time"
override val selectBodyPage = "div.reading-content"
override val selectPage = "img"
override val selectDesc = ".story"
override val selectDesc = ".description"
override val selectGenre = "div.genres-content a"
// Override chapter selector for the detail page
override val selectChapter = "ul.scroll-sm li.item"
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.ALPHABETICAL,
SortOrder.POPULARITY,
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = emptySet(), //not supported
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.UPCOMING),
availableContentTypes = EnumSet.of(
ContentType.MANGA,
ContentType.MANHUA,
ContentType.MANHWA,
ContentType.COMICS,
ContentType.ONE_SHOT,
),
)
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (page > 0) {
append("/page/")
append(page + 1)
}
append("/?post_type=wp-manga")
append("&s=")
filter.query?.let {
append(it.urlEncoded())
}
filter.types.forEach { contentType ->
val typeKey = when (contentType) {
ContentType.MANGA -> "manga"
ContentType.MANHUA -> "manhua"
ContentType.MANHWA -> "manhwa"
ContentType.COMICS -> "comic"
ContentType.ONE_SHOT -> "one-shot"
else -> null
}
typeKey?.let {
append("&type[]=")
append(it)
}
}
filter.states.forEach { state ->
val statusKey = when (state) {
MangaState.ONGOING -> "on-going"
MangaState.FINISHED -> "end"
MangaState.PAUSED -> "on-hold"
MangaState.UPCOMING -> "upcoming"
else -> null
}
statusKey?.let {
append("&status[]=")
append(it)
}
}
append("&sort=")
when (order) {
SortOrder.ALPHABETICAL -> append("title_az")
SortOrder.POPULARITY -> append("most_viewed")
SortOrder.UPDATED, SortOrder.NEWEST -> append("recently_added")
else -> append("recently_added")
}
}
val doc = webClient.httpGet(url).parseHtml()
// Check if we got redirected to the main page (no results)
val currentUrl = doc.location()
if (currentUrl == "https://$domain/" || currentUrl == "https://$domain") {
return emptyList()
}
return parseMangaList(doc)
}
override fun parseMangaList(doc: Document): List<Manga> {
val items = doc.select("div.original.card-lg div.unit")
return items.map { unit ->
val posterLink = unit.selectFirstOrThrow("a.poster")
val href = posterLink.attr("href").toRelativeUrl(domain)
val img = posterLink.selectFirst("img")
val info = unit.selectFirst("div.info")
val titleLink = info?.selectFirst("a")
val title = titleLink?.text()?.trim() ?: "Unknown"
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = img?.src(),
title = title,
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),
authors = emptySet(),
state = null,
source = source,
contentRating = ContentRating.SAFE,
)
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val title = doc.selectFirst("div.info h1")?.text() ?: manga.title
val coverUrl = doc.selectFirst("div.poster img")?.src() ?: manga.coverUrl
val description = doc.selectFirst("div.description")?.html()?.takeIf { it.isNotBlank() }
val metaElements = doc.select("div.meta > div")
var author: String? = null
var status: MangaState? = null
val altTitles = mutableSetOf<String>()
metaElements.forEach { element ->
val label = element.selectFirst("span")?.text()?.trim() ?: return@forEach
val value = element.selectFirst("a")?.text()?.trim() ?: element.ownText().trim()
when {
label.contains("المؤلف") || label.contains("الكاتب") -> author = value
label.contains("الحالة") || label.contains("الوضع") -> {
status = when (value.lowercase()) {
"مستمر", "مستمرة", "ongoing" -> MangaState.ONGOING
"مكتمل", "مكتملة", "completed", "complete" -> MangaState.FINISHED
"متوقف", "متوقفة", "hiatus" -> MangaState.PAUSED
"ملغي", "ملغية", "cancelled", "dropped" -> MangaState.ABANDONED
else -> null
}
}
label.contains("الأسماء البديلة") || label.contains("أسماء أخرى") -> {
value.split(",", "،", ";").forEach { name ->
val trimmedName = name.trim()
if (trimmedName.isNotEmpty()) {
altTitles.add(trimmedName)
}
}
}
}
}
val tags = doc.select("div.genres a, div.tags a").mapNotNullToSet { a ->
val href = a.attr("href").removeSuffix("/").substringAfterLast("/")
val name = a.text().trim()
if (href.isNotEmpty() && name.isNotEmpty()) {
MangaTag(
key = href,
title = name,
source = source,
)
} else null
}
val ratingText = doc.selectFirst("div.rating span.score")?.text()
val rating = ratingText?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN
val chapters = getChapters(manga, doc)
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return document.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirst("a")
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
val isAdult = doc.selectFirst("div.adult-content") != null ||
tags.any { it.key in setOf("adult", "mature", "18+", "ecchi", "smut") }
manga.copy(
title = title,
altTitles = manga.altTitles + altTitles,
coverUrl = coverUrl,
largeCoverUrl = coverUrl,
description = description,
tags = tags,
state = status,
authors = setOfNotNull(author),
rating = rating,
chapters = chapters,
contentRating = if (isAdult) ContentRating.ADULT else ContentRating.SAFE,
publicUrl = fullUrl,
)
}
override suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
return doc.body().select("ul.scroll-sm li.item").mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attr("href").toRelativeUrl(domain)
val link = href + stylePage
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
val name = a.selectFirst(".ch-title")?.text() ?: a.ownText()
val chapterText = a.attr("title").takeIf { it.isNotBlank() }
?: a.selectFirst("span.contain-zeb")?.text()
?: a.ownText()
val name = chapterText.replace("الفصل", "Chapter").trim()
val dateText = li.selectFirst("span.time")?.text().orEmpty()
val scanlator = li.selectFirst("span.user span")?.text()
MangaChapter(
id = generateUid(href),
url = link,
@ -37,13 +266,89 @@ internal class RocksManga(context: MangaLoaderContext) :
number = i + 1f,
volume = 0,
branch = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
scanlator = null,
uploadDate = parseRelativeDate(dateText),
scanlator = scanlator,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val pageContainerSelector = "div#ch-images"
val imageSelector = "img.preload-image"
val container = doc.selectFirst(pageContainerSelector)
?: doc.parseFailed("Page container '$pageContainerSelector' not found on page: $fullUrl")
val imageElements = container.select(imageSelector)
if (imageElements.isEmpty()) {
doc.parseFailed("No images found with selector '$imageSelector' in container.")
}
return imageElements.map { imgElement ->
val absoluteImageUrl = imgElement.requireSrc()
val relativeUrl = absoluteImageUrl.toRelativeUrl(domain)
MangaPage(
id = generateUid(relativeUrl),
url = relativeUrl,
preview = null,
source = source,
)
}
}
private fun parseRelativeDate(dateText: String): Long {
if (dateText.isEmpty()) return 0
val cleanText = dateText.replace("منذ", "").trim()
val cal = Calendar.getInstance()
if (cleanText.startsWith("لحظات") || cleanText.startsWith("لحظة")) return System.currentTimeMillis()
if (cleanText.startsWith("ساعة")) return cal.apply { add(Calendar.HOUR_OF_DAY, -1) }.timeInMillis
if (cleanText.contains("يومين")) return cal.apply { add(Calendar.DAY_OF_MONTH, -2) }.timeInMillis
if (cleanText.startsWith("يوم")) return cal.apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
if (cleanText.startsWith("أسبوع")) return cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
if (cleanText.startsWith("شهر")) return cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
if (cleanText.startsWith("سنة")) return cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
val number = Regex("""(\d+)""").find(cleanText)?.value?.toIntOrNull() ?: return 0
return when {
cleanText.contains("ساعة") || cleanText.contains("ساعات") -> cal.apply {
add(
Calendar.HOUR_OF_DAY,
-number,
)
}.timeInMillis
cleanText.contains("أيام") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
cleanText.contains("أسبوع") || cleanText.contains("أسابيع") -> cal.apply {
add(
Calendar.WEEK_OF_YEAR,
-number,
)
}.timeInMillis
cleanText.contains("شهر") || cleanText.contains("أشهر") -> cal.apply {
add(
Calendar.MONTH,
-number,
)
}.timeInMillis
cleanText.contains("سنة") || cleanText.contains("سنوات") -> cal.apply {
add(
Calendar.YEAR,
-number,
)
}.timeInMillis
else -> 0
}
}
}

Loading…
Cancel
Save