MangaRead parser #17
This commit is contained in:
@@ -23,6 +23,7 @@ enum class MangaSource(
|
|||||||
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java),
|
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java),
|
||||||
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
|
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
|
||||||
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
|
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
|
||||||
NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java)
|
NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
|
||||||
|
MANGAREAD("MangaRead", "en", MangareadRepository::class.java),
|
||||||
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
|
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
|
|||||||
val json = data.substring(pos).substringAfter('[').substringBefore(';')
|
val json = data.substring(pos).substringAfter('[').substringBefore(';')
|
||||||
.substringBeforeLast(']')
|
.substringBeforeLast(']')
|
||||||
return json.split(",").mapNotNull {
|
return json.split(",").mapNotNull {
|
||||||
it.trim().removeSurrounding('"','\'').takeUnless(String::isBlank)
|
it.trim().removeSurrounding('"', '\'').takeUnless(String::isBlank)
|
||||||
}.map { url ->
|
}.map { url ->
|
||||||
MangaPage(
|
MangaPage(
|
||||||
id = url.longHashCode(),
|
id = url.longHashCode(),
|
||||||
|
|||||||
@@ -32,14 +32,18 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
|
|||||||
mapOf("q" to query.urlEncoded(), "offset" to offset.toString())
|
mapOf("q" to query.urlEncoded(), "offset" to offset.toString())
|
||||||
)
|
)
|
||||||
tag == null -> loaderContext.httpGet(
|
tag == null -> loaderContext.httpGet(
|
||||||
"https://$domain/list?sortType=${getSortKey(
|
"https://$domain/list?sortType=${
|
||||||
sortOrder
|
getSortKey(
|
||||||
)}&offset=$offset"
|
sortOrder
|
||||||
|
)
|
||||||
|
}&offset=$offset"
|
||||||
)
|
)
|
||||||
else -> loaderContext.httpGet(
|
else -> loaderContext.httpGet(
|
||||||
"https://$domain/list/genre/${tag.key}?sortType=${getSortKey(
|
"https://$domain/list/genre/${tag.key}?sortType=${
|
||||||
sortOrder
|
getSortKey(
|
||||||
)}&offset=$offset"
|
sortOrder
|
||||||
|
)
|
||||||
|
}&offset=$offset"
|
||||||
)
|
)
|
||||||
}.parseHtml()
|
}.parseHtml()
|
||||||
val root = doc.body().getElementById("mangaBox")
|
val root = doc.body().getElementById("mangaBox")
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import org.koitharu.kotatsu.domain.MangaLoaderContext
|
|||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MangaTownRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
|
class MangaTownRepository(loaderContext: MangaLoaderContext) :
|
||||||
|
RemoteMangaRepository(loaderContext) {
|
||||||
|
|
||||||
override val source = MangaSource.MANGATOWN
|
override val source = MangaSource.MANGATOWN
|
||||||
|
|
||||||
@@ -105,16 +106,17 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposi
|
|||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
description = info.getElementById("show")?.ownText(),
|
description = info.getElementById("show")?.ownText(),
|
||||||
chapters = chaptersList?.mapIndexedNotNull { i, li ->
|
chapters = chaptersList?.mapIndexedNotNull { i, li ->
|
||||||
val href = li.selectFirst("a").attr("href").withDomain(domain, ssl)
|
val href = li.selectFirst("a").attr("href").withDomain(domain, ssl)
|
||||||
val name = li.select("span").filter { it.className().isEmpty() }.joinToString(" - ") { it.text() }.trim()
|
val name = li.select("span").filter { it.className().isEmpty() }
|
||||||
MangaChapter(
|
.joinToString(" - ") { it.text() }.trim()
|
||||||
id = href.longHashCode(),
|
MangaChapter(
|
||||||
url = href,
|
id = href.longHashCode(),
|
||||||
source = MangaSource.MANGATOWN,
|
url = href,
|
||||||
number = i + 1,
|
source = MangaSource.MANGATOWN,
|
||||||
name = if (name.isEmpty()) "${manga.title} - ${i + 1}" else name
|
number = i + 1,
|
||||||
)
|
name = if (name.isEmpty()) "${manga.title} - ${i + 1}" else name
|
||||||
}
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +168,8 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposi
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreatePreferences() = arraySetOf(R.string.key_parser_domain, R.string.key_parser_ssl)
|
override fun onCreatePreferences() =
|
||||||
|
arraySetOf(R.string.key_parser_domain, R.string.key_parser_ssl)
|
||||||
|
|
||||||
private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
|
private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser.site
|
||||||
|
|
||||||
|
import androidx.collection.arraySetOf
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||||
|
import org.koitharu.kotatsu.core.model.*
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
|
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
|
class MangareadRepository(
|
||||||
|
loaderContext: MangaLoaderContext
|
||||||
|
) : RemoteMangaRepository(loaderContext) {
|
||||||
|
|
||||||
|
override val source = MangaSource.MANGAREAD
|
||||||
|
|
||||||
|
override val sortOrders = arraySetOf(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
query: String?,
|
||||||
|
sortOrder: SortOrder?,
|
||||||
|
tag: MangaTag?
|
||||||
|
): List<Manga> {
|
||||||
|
if (offset % PAGE_SIZE != 0) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val domain = conf.getDomain(DOMAIN)
|
||||||
|
val payload = createRequestTemplate()
|
||||||
|
payload["page"] = (offset / PAGE_SIZE).toString()
|
||||||
|
payload["vars[meta_key]"] = when (sortOrder) {
|
||||||
|
SortOrder.POPULARITY -> "_wp_manga_views"
|
||||||
|
SortOrder.UPDATED -> "_latest_update"
|
||||||
|
else -> "_wp_manga_views"
|
||||||
|
}
|
||||||
|
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
|
||||||
|
payload["vars[s]"] = query.orEmpty()
|
||||||
|
val doc = loaderContext.httpPost(
|
||||||
|
"https://${domain}/wp-admin/admin-ajax.php",
|
||||||
|
payload
|
||||||
|
).parseHtml()
|
||||||
|
return doc.select("div.row.c-tabs-item__content").map { div ->
|
||||||
|
val href = div.selectFirst("a").absUrl("href")
|
||||||
|
val summary = div.selectFirst(".tab-summary")
|
||||||
|
Manga(
|
||||||
|
id = href.longHashCode(),
|
||||||
|
url = href,
|
||||||
|
coverUrl = div.selectFirst("img").absUrl("src"),
|
||||||
|
title = summary.selectFirst("h3").text(),
|
||||||
|
rating = div.selectFirst("span.total_votes")?.ownText()
|
||||||
|
?.toFloatOrNull()?.div(5f) ?: -1f,
|
||||||
|
tags = summary.selectFirst(".mg_genres").select("a").mapToSet { a ->
|
||||||
|
MangaTag(
|
||||||
|
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||||
|
title = a.text(),
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
},
|
||||||
|
author = summary.selectFirst(".mg_author")?.selectFirst("a")?.ownText(),
|
||||||
|
state = when (summary.selectFirst(".mg_status")?.selectFirst(".summary-content")
|
||||||
|
?.ownText()?.trim()) {
|
||||||
|
"OnGoing" -> MangaState.ONGOING
|
||||||
|
"Completed" -> MangaState.FINISHED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
|
val domain = conf.getDomain(DOMAIN)
|
||||||
|
val doc = loaderContext.httpGet("https://$domain/manga/").parseHtml()
|
||||||
|
val root = doc.body().getElementById("main-sidebar")
|
||||||
|
.selectFirst(".genres_wrap")
|
||||||
|
.selectFirst("ul")
|
||||||
|
return root.select("li").mapToSet { li ->
|
||||||
|
val a = li.selectFirst("a")
|
||||||
|
MangaTag(
|
||||||
|
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||||
|
title = a.text(),
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val domain = conf.getDomain(DOMAIN)
|
||||||
|
val doc = loaderContext.httpGet(manga.url).parseHtml()
|
||||||
|
val root = doc.body().selectFirst("div.profile-manga")
|
||||||
|
?.selectFirst("div.summary_content")
|
||||||
|
?.selectFirst("div.post-content")
|
||||||
|
?: throw ParseException("Root not found")
|
||||||
|
val root2 = doc.body().selectFirst("div.content-area")
|
||||||
|
?.selectFirst("div.c-page")
|
||||||
|
?: throw ParseException("Root2 not found")
|
||||||
|
val mangaId = doc.getElementsByAttribute("data-postid").firstOrNull()
|
||||||
|
?.attr("data-postid")?.toLongOrNull()
|
||||||
|
?: throw ParseException("Cannot obtain manga id")
|
||||||
|
val doc2 = loaderContext.httpPost(
|
||||||
|
"https://${domain}/wp-admin/admin-ajax.php",
|
||||||
|
mapOf(
|
||||||
|
"action" to "manga_get_chapters",
|
||||||
|
"manga" to mangaId.toString()
|
||||||
|
)
|
||||||
|
).parseHtml()
|
||||||
|
return manga.copy(
|
||||||
|
tags = root.selectFirst("div.genres-content")?.select("a")
|
||||||
|
?.mapNotNullToSet { a ->
|
||||||
|
MangaTag(
|
||||||
|
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||||
|
title = a.text(),
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
} ?: manga.tags,
|
||||||
|
description = root2.selectFirst("div.description-summary")
|
||||||
|
?.selectFirst("div.summary__content")
|
||||||
|
?.select("p")?.drop(1)
|
||||||
|
?.joinToString { it.html() },
|
||||||
|
chapters = doc2.select("li").asReversed().mapIndexed { i, li ->
|
||||||
|
val a = li.selectFirst("a")
|
||||||
|
val href = a.absUrl("href")
|
||||||
|
MangaChapter(
|
||||||
|
id = href.longHashCode(),
|
||||||
|
name = a.ownText(),
|
||||||
|
number = i + 1,
|
||||||
|
url = href,
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val doc = loaderContext.httpGet(chapter.url).parseHtml()
|
||||||
|
val root = doc.body().selectFirst("div.main-col-inner")
|
||||||
|
?.selectFirst("div.reading-content")
|
||||||
|
?: throw ParseException("Root not found")
|
||||||
|
return root.select("div.page-break").map { div ->
|
||||||
|
val img = div.selectFirst("img")
|
||||||
|
val url = img.absUrl("src")
|
||||||
|
MangaPage(
|
||||||
|
id = url.longHashCode(),
|
||||||
|
url = url,
|
||||||
|
source = MangaSource.MANGAREAD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences() = arraySetOf(R.string.key_parser_domain)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 12
|
||||||
|
private const val DOMAIN = "www.mangaread.org"
|
||||||
|
|
||||||
|
private fun createRequestTemplate() =
|
||||||
|
"action=madara_load_more&page=1&template=madara-core%2Fcontent%2Fcontent-search&vars%5Bs%5D=&vars%5Borderby%5D=meta_value_num&vars%5Bpaged%5D=1&vars%5Btemplate%5D=search&vars%5Bmeta_query%5D%5B0%5D%5Brelation%5D=AND&vars%5Bmeta_query%5D%5Brelation%5D=OR&vars%5Bpost_type%5D=wp-manga&vars%5Bpost_status%5D=publish&vars%5Bmeta_key%5D=_latest_update&vars%5Border%5D=desc&vars%5Bmanga_archives_item_layout%5D=default"
|
||||||
|
.split('&')
|
||||||
|
.map {
|
||||||
|
val pos = it.indexOf('=')
|
||||||
|
it.substring(0, pos) to it.substring(pos + 1)
|
||||||
|
}.toMutableMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,29 @@ open class MangaLoaderContext : KoinComponent {
|
|||||||
return okHttp.newCall(request.build()).await()
|
return okHttp.newCall(request.build()).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun httpPost(
|
||||||
|
url: String,
|
||||||
|
payload: String,
|
||||||
|
block: (Request.Builder.() -> Unit)? = null
|
||||||
|
): Response {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
payload.split('&').forEach {
|
||||||
|
val pos = it.indexOf('=')
|
||||||
|
if (pos != -1) {
|
||||||
|
val k = it.substring(0, pos)
|
||||||
|
val v = it.substring(pos + 1)
|
||||||
|
body.addEncoded(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(body.build())
|
||||||
|
.url(url)
|
||||||
|
if (block != null) {
|
||||||
|
request.block()
|
||||||
|
}
|
||||||
|
return okHttp.newCall(request.build()).await()
|
||||||
|
}
|
||||||
|
|
||||||
open fun getSettings(source: MangaSource) = SourceConfig(get(), source)
|
open fun getSettings(source: MangaSource) = SourceConfig(get(), source)
|
||||||
|
|
||||||
fun insertCookies(domain: String, vararg cookies: String) {
|
fun insertCookies(domain: String, vararg cookies: String) {
|
||||||
|
|||||||
@@ -54,4 +54,6 @@ fun LongArray.toArraySet(): Set<Long> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(HashMap<K, V>(size))
|
||||||
@@ -90,7 +90,7 @@ class RemoteRepositoryTest(source: MangaSource) {
|
|||||||
factory {
|
factory {
|
||||||
OkHttpClient.Builder()
|
OkHttpClient.Builder()
|
||||||
.cookieJar(TemporaryCookieJar())
|
.cookieJar(TemporaryCookieJar())
|
||||||
.addInterceptor(UserAgentInterceptor)
|
.addInterceptor(UserAgentInterceptor())
|
||||||
.connectTimeout(20, TimeUnit.SECONDS)
|
.connectTimeout(20, TimeUnit.SECONDS)
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
.writeTimeout(20, TimeUnit.SECONDS)
|
.writeTimeout(20, TimeUnit.SECONDS)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.utils
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.component.inject
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
object AssertX : KoinComponent {
|
object AssertX : KoinComponent {
|
||||||
|
|||||||
Reference in New Issue
Block a user