ReaperScans: Cleanup codes (#13871)

This commit is contained in:
AntsyLich 2022-10-17 17:47:07 +06:00 committed by GitHub
parent 131ce10ccf
commit 0e9c86f901
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 188 additions and 146 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Reaper Scans' extName = 'Reaper Scans'
pkgNameSuffix = 'en.reaperscans' pkgNameSuffix = 'en.reaperscans'
extClass = '.ReaperScans' extClass = '.ReaperScans'
extVersionCode = 36 extVersionCode = 37
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.en.reaperscans package eu.kanade.tachiyomi.extension.en.reaperscans
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -8,17 +9,17 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -75,75 +76,123 @@ class ReaperScans : ParsedHttpSource() {
title = it.text().trim() title = it.text().trim()
setUrlWithoutDomain(it.attr("href")) setUrlWithoutDomain(it.attr("href"))
} }
thumbnail_url = element.select("img").attr("src") thumbnail_url = element.select("img").attr("abs:src")
}
}
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val response = client.newCall(GET(baseUrl)).execute()
val soup = response.asJsoup()
val csrfToken = soup.selectFirst("meta[name=csrf-token]")?.attr("content")
val livewareData = soup.selectFirst("div[wire:initial-data*=frontend.global-search]")
?.attr("wire:initial-data")
?.parseJson<LiveWireDataDto>()
if (csrfToken == null) error("Couldn't find csrf-token")
if (livewareData == null) error("Couldn't find LiveWireData")
val payload = buildJsonObject {
put("fingerprint", livewareData.fingerprint)
put("serverMemo", livewareData.serverMemo)
putJsonArray("updates") {
addJsonObject {
put("type", "syncInput")
putJsonObject("payload") {
put("id", "03r6")
put("name", "query")
put("value", query)
}
}
}
}.toString().toRequestBody(JSON_MEDIA_TYPE)
val headers = Headers.Builder()
.add("x-csrf-token", csrfToken)
.add("x-livewire", "true")
.build()
return POST("$baseUrl/livewire/message/frontend.global-search", headers, payload)
}
override fun searchMangaSelector(): String = "a[href*=/comics/]"
override fun searchMangaParse(response: Response): MangasPage {
val html = response.parseJson<LiveWireResponseDto>().effects.html
val mangas = Jsoup.parse(html, baseUrl).select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
return MangasPage(mangas, false)
}
override fun searchMangaFromElement(element: Element): SManga {
return SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.select("img").first()?.let {
thumbnail_url = it.attr("abs:src")
}
title = element.select("p").first().text()
} }
} }
// Details // Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { override fun mangaDetailsParse(document: Document): SManga {
thumbnail_url = document.select("div > img").first().attr("abs:src") return SManga.create().apply {
title = document.select("h1").first().text() thumbnail_url = document.select("div > img").first().attr("abs:src")
title = document.select("h1").first().text()
status = when (document.select("dt:contains(Source Status)").next().text()) { status = when (document.select("dt:contains(Release Status)").next().text()) {
"On hold" -> SManga.ON_HIATUS "On hold" -> SManga.ON_HIATUS
"Complete" -> SManga.COMPLETED "Complete" -> SManga.COMPLETED
"Ongoing" -> SManga.ONGOING "Ongoing" -> SManga.ONGOING
"Dropped" -> SManga.CANCELLED "Dropped" -> SManga.CANCELLED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
}
genre = mutableListOf<String>().apply {
when (document.select("dt:contains(Source Language)").next().text()) {
"Korean" -> "Manhwa"
"Chinese" -> "Manhua"
"Japanese" -> "Manga"
else -> null
}?.let { add(it) }
}.takeIf { it.isNotEmpty() }?.joinToString(",")
description = document.select("section > div:nth-child(1) > div > p").first().text()
} }
val genreList = mutableListOf<String>()
val seriesType = when (document.select("dt:contains(Source Language)").next().text()) {
"Korean" -> "Manhwa"
"Chinese" -> "Manhua"
"Japanese" -> "Manga"
else -> null
}
seriesType?.let { genreList.add(it) }
genre = genreList.takeIf { genreList.isNotEmpty() }?.joinToString(",")
description = document.select("section > div:nth-child(1) > div > p").first().text()
} }
// Chapters // Chapters
override fun chapterListSelector() = "ul > li" private fun chapterListNextPageSelector(): String = "button[wire:click*=nextPage]"
/** override fun chapterListSelector() = "div[wire:id] > ul[role=list] > li"
* Recursively merges j2 onto j1 in place
* If j1 and j2 both contain keys whose values aren't both jsonObjects, j2's value overwrites j1's
*
*/
private fun mergeLeft(j1: JsonObject, j2: JsonObject): JsonObject = buildJsonObject {
j1.keys.forEach { put(it, j1[it]!!) }
j2.keys.forEach { k ->
when {
j1[k] !is JsonObject -> put(k, j2[k]!!)
j1[k] is JsonObject && j2[k] is JsonObject -> put(k, mergeLeft(j1[k]!!.jsonObject, j2[k]!!.jsonObject))
}
}
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
var document = response.asJsoup() var document = response.asJsoup()
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
document.select("div.pb-4 > div >" + chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content") val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content")
val initialProps = document.selectFirst("div[wire:initial-data*=frontend.comic-chapters-list]")?.attr("wire:initial-data")?.let { val livewareData = document.selectFirst("div[wire:initial-data*=frontend.comic-chapters-list]")
json.parseToJsonElement(it) ?.attr("wire:initial-data")
} ?.parseJson<LiveWireDataDto>()
if (csrfToken != null && initialProps is JsonObject) { if (csrfToken == null) error("Couldn't find csrf-token")
var serverMemo = initialProps["serverMemo"]!!.jsonObject if (livewareData == null) error("Couldn't find LiveWireData")
val fingerprint = initialProps["fingerprint"]!!
var nextPage = 2 val fingerprint = livewareData.fingerprint
while (document.select(popularMangaNextPageSelector()).isNotEmpty()) { var serverMemo = livewareData.serverMemo
var pageToQuery = 1
var hasNextPage = true
while (hasNextPage) {
if (pageToQuery != 1) {
val payload = buildJsonObject { val payload = buildJsonObject {
put("fingerprint", fingerprint) put("fingerprint", fingerprint)
put("serverMemo", serverMemo) put("serverMemo", serverMemo)
// put("updates", json.parseToJsonElement("[{\"type\":\"callMethod\",\"payload\":{\"id\":\"9jhcg\",\"method\":\"gotoPage\",\"params\":[$nextPage,\"page\"]}}]"))
putJsonArray("updates") { putJsonArray("updates") {
addJsonObject { addJsonObject {
put("type", "callMethod") put("type", "callMethod")
@ -151,133 +200,105 @@ class ReaperScans : ParsedHttpSource() {
put("id", "9jhcg") put("id", "9jhcg")
put("method", "gotoPage") put("method", "gotoPage")
putJsonArray("params") { putJsonArray("params") {
add(nextPage) add(pageToQuery)
add("page") add("page")
} }
} }
} }
} }
}.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) }.toString().toRequestBody(JSON_MEDIA_TYPE)
val request = Request.Builder().url("$baseUrl/livewire/message/frontend.comic-chapters-list").method("POST", payload).addHeader("x-csrf-token", csrfToken).addHeader("x-livewire", "true").build() val headers = Headers.Builder()
.add("x-csrf-token", csrfToken)
.add("x-livewire", "true")
.build()
val response1 = client.newCall(request).execute() val request = POST("$baseUrl/livewire/message/frontend.comic-chapters-list", headers, payload)
val responseText = response1.body!!.string()
val responseJson = json.parseToJsonElement(responseText).jsonObject val responseData = client.newCall(request).execute().parseJson<LiveWireResponseDto>()
// response contains state that we need to preserve // response contains state that we need to preserve
serverMemo = mergeLeft(serverMemo, responseJson["serverMemo"]!!.jsonObject) serverMemo = serverMemo.mergeLeft(responseData.serverMemo)
document = Jsoup.parse(responseData.effects.html, baseUrl)
document = Jsoup.parse(responseJson["effects"]!!.jsonObject.get("html")?.jsonPrimitive?.content)
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
nextPage++
} }
document.select(chapterListSelector()).forEach { chapters.add(chapterFromElement(it)) }
hasNextPage = document.selectFirst(chapterListNextPageSelector()) != null
pageToQuery++
} }
return chapters return chapters
} }
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create() return SChapter.create().apply {
with(element) { element.selectFirst("a")?.let { urlElement ->
select("a").first()?.let { urlElement -> setUrlWithoutDomain(urlElement.attr("href"))
chapter.setUrlWithoutDomain(urlElement.attr("abs:href")) urlElement.select("p").let {
chapter.name = urlElement.select("p").first().text() name = it.getOrNull(0)?.text() ?: ""
urlElement.select("p").takeIf { it.size > 1 }?.let { date_upload = it.getOrNull(1)?.text()?.parseRelativeDate() ?: 0
chapter.date_upload = parseRelativeDate(it[1].text())
} }
} }
} }
return chapter
}
// Search
override fun searchMangaSelector(): String = "a[href*=/comics/]"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.select("img").first()?.let {
thumbnail_url = it.attr("abs:src")
}
title = element.select("p").first().text()
}
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException("Not Used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val response = client.newCall(GET(baseUrl)).execute()
val soup = response.asJsoup()
val csrfToken = soup.selectFirst("meta[name=csrf-token]")?.attr("content")
val initialProps = soup.selectFirst("div[wire:initial-data*=frontend.global-search]")?.attr("wire:initial-data")?.let {
json.parseToJsonElement(it)
}
if (csrfToken != null && initialProps is JsonObject) {
val serverMemo = initialProps["serverMemo"]!!.jsonObject
val fingerprint = initialProps["fingerprint"]!!
val payload = buildJsonObject {
put("fingerprint", fingerprint)
put("serverMemo", serverMemo)
// put("updates", json.parseToJsonElement("[{\"type\":\"syncInput\",\"payload\":{\"id\":\"03r6\",\"name\":\"query\",\"value\":\"$query\"}}]"))
putJsonArray("updates") {
addJsonObject {
put("type", "syncInput")
putJsonObject("payload") {
put("id", "03r6")
put("name", "query")
put("value", query)
}
}
}
}.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
return Request.Builder().url("$baseUrl/livewire/message/frontend.global-search").method("POST", payload).addHeader("x-csrf-token", csrfToken).addHeader("x-livewire", "true").build()
}
throw Exception("search error")
}
override fun searchMangaParse(response: Response): MangasPage {
val responseText = response.body!!.string()
val responseJson = json.parseToJsonElement(responseText).jsonObject
val document = Jsoup.parse(responseJson["effects"]!!.jsonObject.get("html")?.jsonPrimitive?.content)
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
return MangasPage(mangas, false)
} }
// Page // Page
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}")
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("img.max-w-full").mapIndexed { index, element -> return document.select("img.max-w-full").mapIndexed { index, element ->
Page(index, "", element.attr("src")) Page(index, imageUrl = element.attr("src"))
} }
} }
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used") // Helpers
private inline fun <reified T> Response.parseJson(): T = use {
it.body?.string().orEmpty().parseJson()
}
// Parses dates in this form: private inline fun <reified T> String.parseJson(): T = json.decodeFromString(this)
// 21 horas ago
// Taken from multisrc/madara/Madara.kt /**
private fun parseRelativeDate(date: String): Long { * Recursively merges j2 onto j1 in place
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 * If j1 and j2 both contain keys whose values aren't both jsonObjects, j2's value overwrites j1's
*
*/
private fun JsonObject.mergeLeft(j2: JsonObject): JsonObject = buildJsonObject {
val j1 = this@mergeLeft
j1.entries.forEach { (key, value) -> put(key, value) }
j2.entries.forEach { (key, value) ->
val j1Value = j1[key]
when {
j1Value !is JsonObject -> put(key, value)
value is JsonObject -> put(key, j1Value.mergeLeft(value))
}
}
}
/**
* Parses dates in this form: 21 hours ago
* Taken from multisrc/madara/Madara.kt
*/
private fun String.parseRelativeDate(): Long {
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
date.contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
date.contains("week") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis contains("week") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }
// Unused
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not Used")
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not Used")
companion object {
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
}
} }

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.extension.en.reaperscans
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class LiveWireResponseDto(
val effects: LiveWireEffectsDto,
val serverMemo: JsonObject
)
@Serializable
data class LiveWireEffectsDto(
val html: String
)
@Serializable
data class LiveWireDataDto(
val fingerprint: JsonObject,
val serverMemo: JsonObject
)