[Madara] Add Madara5 parser

pull/35/head
Koitharu 4 years ago
parent e97992b923
commit 9f75b90325
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -20,6 +20,9 @@ abstract class MangaParser @InternalParsersApi constructor(val source: MangaSour
val config by lazy { context.getConfig(source) } val config by lazy { context.getConfig(source) }
val sourceLocale: Locale?
get() = source.locale?.let { Locale(it) }
/** /**
* Provide default domain and available alternatives, if any. * Provide default domain and available alternatives, if any.
* *
@ -160,4 +163,13 @@ abstract class MangaParser @InternalParsersApi constructor(val source: MangaSour
protected fun parseFailed(message: String? = null): Nothing { protected fun parseFailed(message: String? = null): Nothing {
throw ParseException(message, null) throw ParseException(message, null)
} }
@InternalParsersApi
protected fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? {
return when {
isNullOrEmpty() -> null
size == 1 -> first()
else -> throw IllegalArgumentException("Multiple genres are not supported by this source")
}
}
} }

@ -0,0 +1,178 @@
package org.koitharu.kotatsu.parsers.site
import androidx.collection.arraySetOf
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.InternalParsersApi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
abstract class Madara5Parser @InternalParsersApi constructor(
override val context: MangaLoaderContext,
source: MangaSource,
domain: String,
) : MangaParser(source) {
protected open val pageSize = 22
protected open val tagPrefix = "/mangas/"
protected open val nsfwTags = arraySetOf("yaoi", "yuri", "mature")
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain(domain, null)
@InternalParsersApi
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
val page = (offset / pageSize.toFloat()).toIntUp()
val domain = getDomain()
val url = buildString {
append("https://")
append(domain)
append("/search?s=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
}
append("&post_type=wp-manga")
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
append("&genre%5B%5D=")
append(tag.key)
}
}
append("&op=1&author=&artist=&page=")
append(page)
}
val root = context.httpGet(url).parseHtml().body().selectFirstOrThrow(".search-wrap")
return root.select(".c-tabs-item__content").map { div ->
val a = div.selectFirstOrThrow("a")
val img = div.selectLastOrThrow("img")
val href = a.attrAsRelativeUrl("href")
val postContent = root.selectFirstOrThrow(".post-content")
val tagSet = postContent.getElementsContainingOwnText("Genre")
.firstOrNull()?.tableValue()
?.getElementsByAttributeValueContaining("href", tagPrefix)
?.mapToSet { it.asMangaTag() }.orEmpty()
Manga(
id = generateUid(href),
title = a.attr("title"),
altTitle = postContent.getElementsContainingOwnText("Alternative")
.firstOrNull()?.tableValue()?.text()?.trim(),
url = href,
publicUrl = a.attrAsAbsoluteUrl("href"),
coverUrl = img.attrAsAbsoluteUrl("src"),
author = postContent.getElementsContainingOwnText("Author")
.firstOrNull()?.tableValue()?.text()?.trim(),
state = postContent.getElementsContainingOwnText("Status")
.firstOrNull()?.tableValue()?.text()?.asMangaState(),
isNsfw = isNsfw(tagSet),
rating = div.selectFirstOrThrow(".score").text()
.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
tags = tagSet,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml().body()
.selectFirstOrThrow(".site-content")
val postContent = root.selectFirstOrThrow(".post-content")
val tags = postContent.getElementsContainingOwnText("Genre")
.firstOrNull()?.tableValue()
?.getElementsByAttributeValueContaining("href", tagPrefix)
?.mapToSet { a -> a.asMangaTag() } ?: manga.tags
val mangaId = root.getElementById("manga-chapters-holder")?.attr("data-id")?.toLongOrNull()
?: parseFailed("Cannot find mangaId")
return manga.copy(
description = (root.selectFirst(".detail-content")
?: root.selectFirstOrThrow(".description-summary")).html(),
author = postContent.getElementsContainingOwnText("Author")
.firstOrNull()?.tableValue()?.text()?.trim(),
state = postContent.getElementsContainingOwnText("Status")
.firstOrNull()?.tableValue()?.text()?.asMangaState(),
tags = tags,
isNsfw = isNsfw(tags),
chapters = loadChapters(mangaId),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet(fullUrl).parseHtml()
val arrayData = doc.getElementById("arraydata") ?: parseFailed("#arraydata not found")
return arrayData.html().split(',').map { url ->
MangaPage(
id = generateUid(url),
url = url,
referer = fullUrl,
preview = null,
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = context.httpGet("http://${getDomain()}/").parseHtml().body()
return doc.getElementsByAttributeValueContaining("href", tagPrefix)
.mapToSet { it.asMangaTag() }
}
private suspend fun loadChapters(mangaId: Long): List<MangaChapter> {
val dateFormat = SimpleDateFormat("MMMM dd, HH:mm", sourceLocale ?: Locale.US)
val doc = context.httpGet("https://${getDomain()}/ajax-list-chapter?mangaID=$mangaId").parseHtml()
return doc.select("li.wp-manga-chapter").asReversed().mapIndexed { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
MangaChapter(
id = generateUid(href),
url = href,
name = a.text(),
number = i + 1,
branch = null,
uploadDate = dateFormat.tryParse(
li.selectFirst(".chapter-release-date")?.text()?.trim(),
),
scanlator = null,
source = source,
)
}
}
protected fun isNsfw(tags: Set<MangaTag>): Boolean {
return tags.any { it.key in nsfwTags }
}
private fun Element.tableValue(): Element {
for (p in parents()) {
val children = p.children()
if (children.size == 2) {
return children[1]
}
}
parseFailed("Cannot find tableValue for node ${text()}")
}
private fun String.asMangaState() = when (trim().lowercase(sourceLocale ?: Locale.US)) {
"ongoing" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
else -> null
}
private fun Element.asMangaTag() = MangaTag(
title = ownText(),
key = attr("href").removeSuffix('/').substringAfterLast('/')
.replace('-', '+'),
source = source,
)
@MangaSourceParser("MANGAOWLS", "MangaOwls", "en")
class MangaOwls(context: MangaLoaderContext) : Madara5Parser(context, MangaSource.MANGAOWLS, "mangaowls.xyz") {
override fun getFaviconUrl() = "http://${getDomain()}/frontend/images/favico.png"
}
}

@ -36,11 +36,7 @@ internal abstract class MadaraParser(
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
val tag = when { val tag = tags.oneOrThrowIfMany()
tags.isNullOrEmpty() -> null
tags.size == 1 -> tags.first()
else -> throw IllegalArgumentException("Multiple genres are not supported by this source")
}
val payload = createRequestTemplate() val payload = createRequestTemplate()
payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString() payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString()
payload["vars[meta_key]"] = when (sortOrder) { payload["vars[meta_key]"] = when (sortOrder) {

@ -100,4 +100,8 @@ fun Element.requireElementById(id: String): Element {
fun Element.selectLast(cssQuery: String): Element? { fun Element.selectLast(cssQuery: String): Element? {
return select(cssQuery).lastOrNull() return select(cssQuery).lastOrNull()
}
fun Element.selectLastOrThrow(cssQuery: String): Element {
return selectLast(cssQuery) ?: throw ParseException("Cannot find \"$cssQuery\"")
} }

@ -8,6 +8,8 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession
internal class MangaLoaderContextMock : MangaLoaderContext() { internal class MangaLoaderContextMock : MangaLoaderContext() {
@ -26,6 +28,7 @@ internal class MangaLoaderContextMock : MangaLoaderContext() {
.cookieJar(cookieJar) .cookieJar(cookieJar)
.addInterceptor(UserAgentInterceptor(userAgent)) .addInterceptor(UserAgentInterceptor(userAgent))
.addInterceptor(CloudFlareInterceptor()) .addInterceptor(CloudFlareInterceptor())
.hostnameVerifier(ConsumeAllVerifier())
.connectTimeout(20, TimeUnit.SECONDS) .connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS)
@ -61,4 +64,10 @@ internal class MangaLoaderContextMock : MangaLoaderContext() {
cookieJar.loadFromStream(it) cookieJar.loadFromStream(it)
} ?: println("No cookies loaded!") } ?: println("No cookies loaded!")
} }
private class ConsumeAllVerifier : HostnameVerifier {
override fun verify(hostname: String?, session: SSLSession?): Boolean {
return true
}
}
} }
Loading…
Cancel
Save